Org Mode requests: [TASK] Re: orgmode: do textsec-url-suspicious-p for http and https links
-1:-- [TASK] Re: orgmode: do textsec-url-suspicious-p for http and https links (Post Org Mode requests)--L0--C0--2026-10-10T10:09:02.000Z
-1:-- [TASK] Re: orgmode: do textsec-url-suspicious-p for http and https links (Post Org Mode requests)--L0--C0--2026-10-10T10:09:02.000Z
For more than a decade the Clojure REPL most people saw in a terminal was
REPLy, the one behind lein repl. It
served us well, but it’s showing its age, and I kept wishing for a terminal
REPL that felt like CIDER, without the Emacs part. So I finally built one.
neorepl 0.1 is out today!
There’s no shortage of Clojure REPLs, so it’s fair to ask. Here’s what I was after:
rebel-readline and bb repl --connect are both great, and I borrowed from
them freely, but neither tries to be an nREPL-first tool with CIDER’s tooling,
jack-in and a CLI for agents. That combination is the whole point of neorepl.
Here’s the REPL in action:

A few things worth pointing out:
,doc, ,source, ,apropos,
,macroexpand, ,pst, ,reload, ,edit (which opens a definition in your
$EDITOR) and ,help for the rest. The Clojure reader sees a comma as
whitespace, so they can’t clash with real code.*in* gets its input,
something REPLy never quite got right.Without a server to connect to, neorepl starts one for the project, the way
CIDER’s jack-in does, with cider-nrepl on the classpath, and stops it when you
exit. It knows Leiningen, the Clojure CLI, babashka and shadow-cljs projects.
ClojureScript works too. Start a ClojureScript REPL in the session (through
shadow-cljs or piggieback, which jack-in adds for you) and neorepl follows
along: evaluation, completion, docs and the rest switch to ClojureScript, and
:cljs/quit takes you back.
Coding agents are surprisingly good at Clojure, as long as they have a REPL to talk to. Most of them drive CLIs much better than anything else, so neorepl has a CLI that’s meant to be driven:

neorepl eval evaluates code on the project’s server. Calls share a
session, so *ns*, *1 and *e carry over from one call to the next, and
--session keeps separate lines of work apart.--format json (or edn) gives you the values, the output and the
namespace as data.--timeout interrupts the evaluation before giving up, so an infinite loop
doesn’t take the server down with it, and --max-output keeps a stray
(range) from eating the agent’s context.neorepl check finds unbalanced parens, brackets and braces without a
server. That’s the most common way for an agent (or a human) to break a
Clojure file.neorepl doc, source, apropos, complete, where, macroexpand and
reload do what the REPL’s commands do.neorepl server starts a server that stays up, for editors, agents and
neorepl eval to share.The exit codes are boring on purpose: 0 when all went well, 1 for an
evaluation error, 3 when there’s no server to talk to and 124 for a timeout
(like timeout(1)).
A new client is also a new way to break servers, or to be broken by them, and the nREPL world has quite a few servers these days. Fortunately I’ve been working on proof, a compatibility suite for nREPL, which made neorepl a good test subject.
proof checks both sides of the conversation. proof proxy sits between a
client and a server and grades everything the client sends: every request has
an id, need-input gets answered with stdin in the right session, and so on.
proof serve is a small nREPL server that can behave like other servers in
very specific ways - sending only the last value of the code like Basilisp and
jank, dropping stderr, having no interrupt op, splitting output into one
message per character, and so on. I ran neorepl’s REPL and CLI through both,
against nREPL, babashka, Basilisp and jank, and through every quirk proof
serve knows about.
neorepl came out of it in decent shape, but not unscathed:
neorepl eval -) that then read *in* itself crashed
neorepl and left the evaluation waiting on the server for input that was
never coming.neorepl eval exited with 1 when it lost the server, which is the status
for code that failed.All of those are fixed in 0.1. It also turned up a couple of things that are
nREPL’s fault rather than neorepl’s: need-input and the end of an
interrupted evaluation can go to the wrong connection when a session is used
from more than one. Those will be fixed in nREPL itself.
It works the other way around too. proof now checks servers with the exact requests neorepl sends while starting its REPL and evaluating code, next to the ones CIDER, Calva, Conjure, vim-fireplace and REPLy send, so a server that breaks neorepl finds out from proof, not from a bug report. nREPL, babashka, Basilisp and jank all pass those checks today.
Building neorepl turned up a few problems elsewhere, which is one of my favourite side effects of writing a new client. piggieback used to evaluate only the first form you sent it and silently drop the rest, and it loaded the ClojureScript compiler at startup even when nobody asked for a ClojureScript REPL, which added almost half a second to the startup of every server with ClojureScript on the classpath. The fixes for both (piggieback#162 and piggieback#161) will be in the next piggieback release. The Node.js REPL in ClojureScript itself doesn’t cope well with interrupted evaluations, and that one is next on my list.
0.1 is the foundation. Here’s what I’d like to tackle next:
The easiest way to install neorepl is bbin:
$ bbin install io.github.nrepl/neorepl
$ cd my-project
$ neorepl
The README has the details.
It’s early days, so I’m sure you’ll find rough edges. Please file issues,
and tell me what you’d like to see next. And if you’re one of the people who
kept lein repl and REPLy going over the years - thank you! neorepl wouldn’t
exist without you.
Keep hacking!
-1:-- neorepl: A Terminal REPL for Clojure (and Friends) (Post Meta Redux)--L0--C0--2026-10-10T09:38:00.000Z
-1:-- Repeating commands (Post Marcin Borkowski)--L0--C0--2026-10-10T05:14:56.000Z
The topic that wouldn’t die. It started with a quick post on Sacha’s solution to a problem posed by David Pape. I thought it was a nice solution but then 0-_-0_ pointed out that abo-abo had already solved this problem with his tiny library. So I wrote about that and prepared to move on.
I was not allowed to. Frequent commenter Phil remarked in my original post that he would use a keyboard macro. He suggested that that would make sense for those of us who who seldom need to enter a sequence of ranges like Pape. In Pape’s case this was something that he needed to do a lot so a solution like Sacha’s or abo-abo’s make sense. I agree with Phil. A keyboard macro is exactly how I would solve this problem given that I seldom have it.
This seems pretty obvious and might not be worth writing about except that Phil’s solution showed me something that I didn’t know: You can set the initial value of the counter with a Ctrl+u n where n is the desired initial value. That’s a lot easier than using Ctrl+x Ctrl+k Ctrl+c to do the job.
As a final note, I think that Phil had a typo in his macro. As far as I can tell it should be
C-u 47 <f3> # <f3> , SPC <f4> <f4> <f4> <f4> <f4> <f4> <f4> <f4>
At least that’s what works for me.
It turns out that I regularly use a macro with a counter so I’m really happy to learn that the counter can be set so easily.
Update : Fixed link.
-1:-- Still More On Entering Ranges (Post Irreal)--L0--C0--2026-10-09T15:34:21.000Z
In a nice coda to yesterday’s post on Sacha Chua’s solution for entering number ranges, 0-_-0_ points out that a solution already exists. That solution is abo-abo’s tiny library. I vaguely remember seeing tiny when abo-abo released it 13 years ago but I never followed up and used it. Still, anything from abo-abo is worth taking a look at and, in any event, we now have two solutions to the problem.
Abo-abo’s solution brings to mind another Emacs lesson: very often, if you need some functionality, there’s a good chance that it already exists. Of course, that’s not the same as being easily discoverable. An internal function can usually be discovered through the on-line documentation but a third party library like tiny won’t appear in that documentation until it’s loaded.
That’s it for today. I just wanted to tell the rest of the story from yesterday.
-1:-- More On Number Ranges (Post Irreal)--L0--C0--2026-10-08T15:33:30.000Z
-1:-- Fixing the Emacs 31+ Tree-sitter syntax highlighting issue in indirect buffers (The Emacs maintainers will not fix it, so I did) (Post James Cherti)--L0--C0--2026-10-08T12:45:56.000Z
I use org-use-speed-commands all the time when navigating and performing actions on org headlines, but I think I have found a friction point, something that just feels a little off.
When the point is on * and then you press the key t, it calls org-todo via org-self-insert-command. Fast, but the feel is off, the point stays on the stars, so I have to move right before I can type the headline.
The reason is in org-todo itself. It has a cursor fixup that parks point just after the keyword, but only when point is not at beginning of line. Speed keys by definition run at beginning of line (org-speed-command-activate requires (bolp)), so the fixup never fires for exactly the keys that would benefit most.
There is no custom option for it, so I added a small :after advice:
(defun my/org-todo-stop-after-keyword (&rest _)
"Move point to just after TODO keyword when `org-todo' leaves it at BOL.
Speed command `t' (via `org-self-insert-command') runs with point at
beginning of headline, where `org-todo' skips its cursor fixup."
(when (and (bolp) (org-at-heading-p))
(when (looking-at org-todo-line-regexp)
(goto-char (or (match-end 2) (match-end 1)))
(when (looking-at " ") (forward-char 1)))))
(advice-add 'org-todo :after #'my/org-todo-stop-after-keyword)
Now * Test with point on * plus t becomes * TODO Test with point on Test, ready to type. Same for cycling * TODO to * DONE.
Small change, but speed keys finally feel like they finish the job instead of leaving me parked on the margin!
-1:-- Org Speed Keys: Making t Land After the TODO Keyword (Post James Dyer)--L0--C0--2026-10-08T10:03:00.000Z
gravity
Product: Gravity
Price: $9.99
We are all familiar with the act of note-taking. A thought occurs to you, you switch to your note-taking application and face a slew of decisions:
Our note-taking systems have evolved into a quagmire. We are now in the business of managing our systems. Instead of taking notes, we work incessantly on our note-taking system. Soon we avoid taking notes, it is too much bother.
Look at the discussions on the Zettelkasten Forum:
Or go to Obsidian Forum. You will see the same discourse. People obsessing over the system.
I had heard of Andrej Karpathy. Couldn’t tell you in what context, but the name was familiar to me. He wrote the following: The append-and-review note – karpathy.
Go read it, it is a small read. I will wait.
His system is two steps: Append and Review. Append to a single file. No tags, no metadata of any kind. This is the line which stood out to me: “When I note something down, I feel that I can immediately move on, wipe my working memory, and focus fully on something else at that time. I have confidence that I’ll be able to revisit that idea later during review and process it when I have more time.”
He periodically reviews his notes moving things which are important at the moment to the top. The rest of the information continues to sink to the bottom, like gravity.
The main advantage of this system? No friction. No decisions at the moment of entering a note/thought. Nothing lost. Nothing falls through the cracks.
Gravity is an attempt to provide an environment for this system to work. It can be a menu-bar only application. You press a keyboard command wherever you are. Up pops an editor window. You type or paste in what your thought was. Hit ⌘↵ or three ↵’s. The note is saved. You are back to the program you were in initially.
Periodically you can review your notes. The ones which are more relevant you can nudge up the chain. Everything else keeps getting demoted and drops down the list, like gravity.
Gravity is powered by a SQL database as its back end. You can export your whole repository of notes to a Markdown file, a text file, or a JSON file. So even though it is a database, there is no lock in to the product.
I was intrigued by the following line from the developer, Tristan Manchester:
Gravity has one job: minimize the time between “I have a thought” and “the thought is captured and I can move on.”
That appealed to me and I got curious.
gravity - window
A keyboard command, you can customize, from anywhere and this is the Gravity window. All of your thoughts and their content in a window.
gravity - capture
This is the capture window. A keyboard command, customisable, and this window pops up. You enter your thoughts, hit the return key twice, or ⌘+↵ and the content is saved to Gravity. You hit ⇧+↵ key to add a new line. (I notice that a simple ↵ does add a new line too).
In the Gravity window, you have the choice of promoting any entries you need to the top of the list. If you have a long list, this makes items which are relevant to you be readily accessible and it is the premise behind the program. Items which are not relevant for the time being keep sinking to the bottom.
You have the option of archiving or deleting thoughts you want to keep or want to discard.
It is all done in Gravity.
They are well thought out and well designed.
pref - general
You will want Gravity to launch at login. You want it to be always available. I don’t much care for the program to show up in the Dock. You can choose to have the two ↵’s to save the capture or rely on the usual ⌘+↵ command. I like having both.
pref - keyboard 1
pref - keyboard 1
pref - keyboard 1
The program is fully keyboard driven. I love this aspect of the program and they are all customisable. You set your own or stick to the ones shipping with the program. Regular readers know how much I like this feature.
pref - syncing
You can choose to sync to iCloud. That allows the same document in multiple machines. I love it.
pref - search
You can choose to have the archives included in the search. If you have a long list of entries in the current focus and want to move some over to the archive, you can still include them in the search and nothing is left out of your searches.
pref - advanced
This is where you can export your whole collection of notes to a Markdown file, a plain text file or a JSON file.
pref - more advanced
You can monitor some stats about your usage of the program. Optimize your SQL database. There is also a command line tool (I haven’t explored this much. Although I have installed it).
This is from the developer of Gravity:
I get the same feature request about once a week. “Love Gravity, but could you add folders?” “Would be perfect if I could tag notes.” “Any plans for linking between notes?” “An API would be amazing.” The answer to all of these is no. Not “not yet”. Just no.
This probably sounds like bad product thinking. Aren’t you supposed to listen to users? Give them what they ask for? Iterate toward their needs?
Usually, yes. But there’s a specific reason Gravity works the way it works, and adding these features would break it. The limitations aren’t gaps. They’re the design.
Someone recently asked me why Gravity doesn’t have folders. My first instinct was to say “we’ll add that eventually.” Then I caught myself. We’re not adding folders. We’re not adding tags either, or bidirectional links, or a plugin system, or templates, or a knowledge graph. These aren’t items on a roadmap we haven’t gotten to yet. They’re deliberate omissions. The missing features are the product.
I found this interesting. Mind bending, but interesting. Then I remembered:
Gravity has one job: minimize the time between “I have a thought” and “the thought is captured and I can move on.”
If you are looking for a capture application which is designed to do just that, have a thought, capture a thought, move on with life, then Gravity is the ideal tool for that need.
It is well-designed software which does that and does that well.
I am old. I forget things. There are too many times during the day when I have a thought and it floats away a few minutes later. For instance, read the Org manual and read the section on narrow and widen. A few minutes later, I have no recall of what I was thinking about. My solution? Capture the thought. Do I want to capture the thought in a stream of thoughts? Or do I want to categorize the thought into a file?
The program I use for this is Capture for Mac. I have a text file which has markdown headings like:
The Capture window in Capture gives me the option of inserting the content under a specific heading. It adds a time stamp and some context if I have highlighted something. That is what I use for capture at this point.
You can follow a similar arrangement in Gravity. Prepend some text to the capture. For instance, things about Emacs, can have Emacs: to start with, or Read: for things to read later or Watch: to watch later, and so on. And the system works because the Search function in Gravity is excellent.
I understand that the goals of the two programs are different. They are not solving the same problem. If your need is to record thoughts and move on with your life, Gravity as a system is perfect for that. I love working in it. I admire what it is trying to do and feel it is perfectly designed for that.
("o" "OBTF" entry
(file+headline "~/Dropbox/org/obtf.org" "OBTF")
"Created: [%(format-time-string \"%Y-%m-%d %H:%M\")]\n %?"
:prepend t
:empty-lines-before 1
:empty-lines-after 1)
You know I have to do everything in Emacs. I added this Org-capture template which lets me perform a similar function in Emacs. I can capture content to a file called OBTF.org. I can then refile content to the top or the bottom of the file through danielsz/Palimpsest. I can have an Archive heading and move sections to it through org-refile. All of this is more effort than Gravity, but it has the added advantage of being in Emacs.
Blog — Thinking About Capture — Gravity
No, We’re Not Adding Folders — Gravity
Gravity is a fantastic application to capture your thoughts. It is a product with a defined philosophy and it sticks to its vision.
I recommend Gravity heartily for your capture needs.
macosxguru at the gmail thingie.
Note: Thanks to Tristan Manchester, the developer of Gravity, for providing me with a promo code when I asked for one.
-1:-- Using Gravity For Your Thoughts (Post Bicycle for Your Mind)--L0--C0--2026-10-08T03:52:01.000Z
Raw link: https://www.youtube.com/watch?v=KuxjRH6RF0U
[ The stream will be recorded. You can watch it later. ]
This Saturday, 10 October 2026, at 16:00 Europe/Athens time, I will do an “ask me anything” live stream. I have been meaning to do this for a long time, but I never found the chance for it.
Ask me a question about anything—literally ANYTHING—and I will give you an honest answer (e.g. Emacs, philosophy, technology, keyboards, politics, the hut, Français, and more!). It does not matter if the question is personal or sensitive. I will give you a complete answer to the best of my abilities. If you need any ideas, check the rest of my website, where I write at length on a broad range of topics.
The plan is to do a live stream for at least 1 hour. If there are many questions, I will extend it to 2-3 hour or, generally, for as long as needed. And if there are no questions, then I can do something related to Emacs or close the stream earlier—no problem!
-1:-- Live: Ask me ANYTHING (2026-10-10 16:00 Europe/Athens) (Post Protesilaos)--L0--C0--2026-10-08T00:00:00.000Z
Liens :
Je presse la touche zéro du pavé numérique avant et après avoir dit « Rappelle-moi demain de préparer une présentation pour Emacs Paris sur les commandes vocales. » La fonction crée une tâche dans ma boîte de réception. La tâche est planifiée pour demain. Il y a un lien vers l'enregistrement
« Rappelez-moi demain de préparer une présentation pour Emacs Paris sur les commandes vocales », la capture d'écran et le contexte de tâche. Ça m'aident si j'ai été mal comprise ou si j'ai besoin de contexte. De la même manière, je peux dire « Rappelle-moi la semaine prochaine de faire la paperasse pour l'école. » « Rappelle-moi aujourd'hui de créer une vidéo sur mes commandes vocales. »
Je peux aussi créer une tâche et déclencher le suivi du temps (ou clock in). Par exemple, je peux dire « Commence à implémenter la génération des questions. »
Si je perds le fil de mes pensées : « j'en étais où ? » Ça m'affiche les tâches récentes.
« Je faisais quoi ? » Ça bascule sur la tâche actuelle. Voilà des commandes vocales pour ajouter des tâches.You can e-mail me at sacha@sachachua.com.
-1:-- Des commandes vocales pour ajouter des tâches sur Emacs (Post Sacha Chua)--L0--C0--2026-10-07T19:52:45.000Z
Will voice kill the keyboard. No. Will the way we interact with Emacs, and computers in general, change drastically? No.
Star Trek has a mythical AI/computer that responds to voice commands and interacts with you to clarify or note other work that may pertain to what you wanted. Evidently the interface is so ubiquitous that Scotty talks to a mouse noting it's "quaint" to need to push it and type on a keyboard to interact with a computer.
But if we watch the bridge of any Star Trek show people aren't talking the whole time. They're tapping screens to get the computer to do things for them. Theoretically the captain could tell the ship where he wants to go and let the computer do it, but he issues a command and then someone types in the needed commands to the ship so it goes in the expected direction.
I work at home in a solo office space. I could talk to my computer and it would disturb no one, except me. I'd be unable to listen to music on my stereo at the same time just in case the computer couldn't understand my words versus the song.
I don't think keyboards are going anywhere any time soon. They provide an unambiguous interface to your computer that doesn't disturb others. I think that those loving Emacs interactions like they use it will continue to do so as they do now because it's the most efficient way to control the Emacs interface.
-1:-- Emacs Carnival - Will we change how we interact with Emacs? (Post Curtis McHale)--L0--C0--2026-10-07T17:55:00.000Z
The incomparable Sacha Chua has a very nice solution to a problem posed by David Pape: how can we enter a range of numbers as list of distinct numbers in Emacs? Pape’s specific problem was entering the numbers of error reports that a Git commit closes. Rather than entering something like
closes: 47, 48, 49, 50, 51, 70
he’d like to enter something like
closes: 47-51 70
and call an Emacs function to have it expanded into the first example.
Chua has a very nice solution that covers that case as well things like
47-51, 70
or even
5-15/2
to expand into the sequence
5, 7, 9, 11, 13, 15
Most of the heavy lifting is done by the number-sequence function. The rest of the work is parsing the input string into either a single number or a range of numbers that number-sequence can work on.
Chua’s solution strikes me as an example of Emacs at its best. The problem it solves is not earth shattering. It’s simply a bit of friction in a user’s everyday workflow but Emacs easily provides a solution that makes the friction disappear. Take a look at her code. It not only shows an ingenious solution to the problem but also demonstrates the power of Emacs and Elisp to make the application of that genius simple.
-1:-- Inserting Number Ranges As A List (Post Irreal)--L0--C0--2026-10-07T15:40:09.000Z
October's Emacs Carnival theme "Are keyboards and M-x here to stay?" is a good nudge to think about the voice interface I've been experimenting with and where I want to take it.
Like Chris Maiorana, I love the way Emacs's M-x (execute-extended-command) allows me to call any command by name.
It means that I can call commands or come up with my own commands without worrying about assigning them to keyboard shortcuts or making sense of where to place them in a hierarchical menu.
With flexible completion via the vertico and orderless packages, I don’t even need to type in the full name of the command or remember the words in the right order.
If I still can’t remember the command, it’s easy enough to use defalias to make my own nickname for it.
I tend to limit keyboard shortcuts to only my most common commands.
Everything else is hard to practise enough so that it sticks in my muscle memory.
Even with marginalia showing me keybindings when I use M-x and which-key showing me hints when I pause, it doesn’t sink in.
Maybe I could use key-quiz to drill a small number of keyboard shortcuts I’m actively working on learning, or modify help-quick-sections to show me reminders when I use C-h C-q (help-quick-toggle).
Getting one keyboard shortcut to do the right thing in different
contexts is great because it means fewer shortcuts to memorize. Sometimes I can do that by binding the right functions to different mode maps with bind-key, like the way my sacha-org-insert-link-dwim uses the right link syntax in Markdown or OddMuse too.
I also like the context-sensitive keyboard shortcuts I get from Embark.
I find myself using a handful of those regularly, and C-.
(embark-act) starts a bunch of shortcuts across different types of
things: i to insert, w to copy, and so on.
For example, I know C-x C-f (find-file) will let me select a file to open.
Instead of opening it, I can use C-. i to insert the filename into the buffer.
Simpler keyboard shortcuts help. I rarely use the numpad on my laptop keyboard, so those keys can be mapped to functions that don't need to be followed by lots of typing. For example, 0 is a large button. I use it to start and stop speech recognition. Actually, I bind f9 in Emacs to sacha-whisper-run-at-point, and I bind numpad-0 as a keyboard shortcut in KDE (my window manager) instead of just in Emacs so that it's available globally. I'm still on X11 instead of Wayland. The shell script xdotool-emacs focuses Emacs briefly and sends the keys, and the shortcut is set to /home/sacha/bin/xdotool-emacs key --clearmodifiers F9. I do that instead of using emacsclient because emacsclient starts a different buffer, and I might want to keep the context. My sacha-whisper-maybe-type function detects if Emacs is not the focused application and types the text if it isn't, which means I can use it as keyboard input for oher applications while still taking advantage of the corrections and commands I've defined in Emacs. An easy-to-press global shortcut means I can use it from anywhere, even when my foreground app is the browser or another application.
An additional modifier opens up more possibilities for keyboard shortcuts near the regular typing position. My Windows key acts like a Super modifier. I've set it up as a one-shot modifier with Kanata so that I can either hold it down to access more shortcuts or tap it to treat it like a leader key.
There are quite a few commands for which neither M-x nor keyboard shortcuts feel like the right fit: things I want to do faster or more fluidly than M-x, but which I don't do often enough to memorize a good shortcut for. I've been experimenting a bit with voice input, and I really like it.
French: I like challenging my brain and I want to help the kiddo learn it too. Improving my speaking will also make it easier for me to listen to myself and close the feedback loop. Maybe I can even use speech recognition to give me feedback and increase motivation while speaking, like keeping track of words or phrases I want to use. When I use them in conversation, the buffer can cross them off the list, which lets me focus on the words remaining.
For context, I use Linux on a Lenovo P52 laptop (manufactured 2018, 64 GB RAM but hardly any GPU), and I have an Android phone. I like writing regular text, Emacs Lisp, and some Python and JavaScript. I'm slowly starting to have more focused time now that the kiddo is in virtual school, although it's still broken up by recess and lunch. I will probably have an increasing amount of focused time over the next few years.
I love the way voice reduces context shifts. For example, some of my most-used voice commands are:
remind me [today/tomorrow/next week/next month/someday] to…: capture and schedule the task; include an Org link and screenshot for context, and link to the audio recording in case the text was misrecognized
where was I?: show recently-clocked tasks
I don't have to think about which keys I've configured in org-capture-templates. I just use a natural phrase.
I've defined "open …" to open things from my bookmarked links stored in an Org file. Because my numpad-0 keybinding works from anywhere, I can even use it to open sites or capture notes while looking at my browser.
Because Emacs can interact with lots of other applications, I can even experiment with controlling those applications by voice. For example, I use obs-websocket.el to control OBS for streaming or recording a video. Using "Let's go live" and "Over and out" to start or stop streaming or "Start recording" and "Stop recording" to manage the recording means I can do that without needing to find the right window. I stream with a 10-second delay so that there's a little bit of time to catch any private information that's been accidentally shared on the screen. My new "panic" command lets me discard that delay and interrupt the stream by using an OBS keyboard shortcut to kill the stream immediately, even if I'm probably not going to remember that exact keyboard shortcut when I'm stressed. It should be a little easier to remember that than a keyboard shortcut I hardly ever use or a menu item that is a really small target (and my Emacs might not even be my foreground app).
For regular dictation, I'm experimenting with having it automatically place each sentence on a new line or list item. This makes it easy to reorganize in Org. I can refill it into a paragraph when I'm done.
I still have to deal with the limits of my memory, of course.
I have a "what can I say" command that opens up an Org file with my notes. I keep this as a manual note instead of just dumping the contents of my sacha-whisper-commands variable so that I can prioritize the things I'm trying to build as habits.
Here's the part of my config that sets up some speech commands.
I might want more complex grammar someday for things that regular expressions can't easily handle. That's something other projects have done a good job of figuring out.
There's been a lot of good work on voice input over the years, especially since a number of Emacs users have a hard time with or would like to avoid repetitive stress injury. In addition to the innumerable projects that let you use Whisper or similar speech recognition engines to dictate prose, there are also a few projects that use a constrained vocabulary to more reliably control Emacs. Two projects in this area are based on the Talon voice control system: Voicemacs and Emacslisten.
M-x t-d-on-e runs toggle-debug-on-error, as defined in community/apps/emacs/emacs_commands.csv. I seem to be having a hard time with getting orderless to do that with orderless-flex, so I'm just going to remove that column of the CSV from my emacs_commands.csv.~/.talon/user directory. It lets Emacs and Talon communicate via RPC to send additional context. It also extends some things like dired and company to make them more voice-friendly.I'm new to both projects, so I'm not sure if I'm reading this right, but I think I might be able to use Emacslisten to define voice commands with Emacs Lisp, and then maybe someday use Voicemacs to provide richer context for some of those commands. For example, Voicemacs can list snippets, but I'm not quite sure how to use that yet.
It looks like I can use Talon for commands and my whisper.el setup for regular dictation at the same time. The Pipewire virtual sinks help with that, I think, since the programs don't have to have an exclusive lock on the audio device. This means that I can explore the kinds of vocabularies that people have set up while also taking advantage of greater accuracy and ease of correction in my whisper dictation workflow. I think I can use the Talon process socket to turn off recognition when I want to dictate using my whisper.el setup, and then I can turn it back on afterwards.
A note about the underlying speech recognition: Talon is cross-platform and free, but not open source. Subscribing to Talon (CAD 36/month) lets you use a Conformer + Whisper hybrid engine that uses whisper-large for more accuracy, but that's a bit on the expensive side for me. Dragonfly is a free open source engine that can use Kaldi or CMU Pocket Sphinx for speech recognition, but I'm not sure what the performance is like.
Interestingly, because Talon simulates keystrokes, using the commands from the community set means keys sometimes arrive out of order on my computer. "split solo" is supposed to run M-x delete-other-windows, but it sometimes ends up sending M-x delete-other-wnidows instead. This is the same sort of annoyance that gets in the way of typing quickly on my computer. Keystrokes get processed out of order when the CPU gets busy, which is part of the reason why I find dictation to be more reliable than typing sometimes. I should fix that someday, but my attempts so far aren't working. (I wonder if it has something to do with the GPU under Linux; maybe I should drop back down to CPU only since it turns out I can't use the GPU for much anyway.) Quick fix, let's try adding a brief delay in my settings.talon:
settings():
# Adds a small pause (in ms) between each character typed
insert_wait = 5
# Ensures modifiers (Shift/Ctrl) don't un-latch too fast
key_wait = 1.0
I think Voicemacs uses a sevver and and Emacslisten uses emacsclient to communicate with Emacs directly, so they probably won't run into this keystroke problem.
It might be interesting to see if I can port some of my simpler commands over to Emacslisten or straight Talon so that they're always active. Then I can slowly work on getting used to the other commands people find useful, or incorporating them into my setup.
I love shifting fluidly from one form of input to another, depending on what makes sense at that moment.
For example, I often start my thoughts by drawing on a light dotted grid using Noteful on my iPad. That lets me quickly cluster ideas spatially, drawing connections and highlighting words as I figure out what I want to say.
Then I use dictation and typing to sketch the outline, jumping around as thoughts occur to me. If I'm livestreaming at the same time, that helps me demonstrate things along the way and lets me benefit from people's questions. Dictation lets me explain things to people and capture the text at the same time.
I can move around with the keyboard, but it would be really cool to use voice for quick navigation between sections of my outline using either line numbers or words. I've implemented a "line" command that lets me jump to a line using the last two digits of its line number, since I probably won't have more than 100 lines visible on the screen at a time. For example, this line is currently 119, so referring to it as "nineteen" would work. I'd like to add more operations for editing the text. I think it would be great to be able to say "kill line 33" to delete that line or "move lines 16 to 20 after 27" to have it move those lines there.
I now have a sacha-whisper-one-sentence-per-line variable that splits up dictation if non-nil, so we'll see where those take me. Orukeet does a reasonable job of punctuating my sentences if I don't pause too much between phrases. My M-q re-wraps paragraphs between one sentence per line, one paragraph per line, and wrapping to fill-column, and it's probably the sort of thing that more voice commands would make even more convenient.
Maybe I can even use that spatial sense of a layout by touching buttons in an Emacs-served webpage on my tablet to direct my dictation to different places. Org-draw uses a similar Emacs web server approach to let you draw on another device and embed the results into an Org file, and there are also simpler HTTP servers (web-server, simple-httpd), so it's probably quite doable. I'm imagining something that takes the top-level items from my current nested list and makes them nice big buttons, so I could have something like "Introduction", "Keyboard", "Reasons", "Experiment", "Hybrid", and "Conclusion", and clicking on those could trigger a POST request to the server inside Emacs which moves my point accordingly. I might even have buttons for various commands. It sounds pretty straightforward to implement.
When my brain comes up with lots of interesting ideas, my org-capture voice commands let me choose which rabbit holes to postpone and which ones I want to dive into.
If I get interrupted by life, I can write on my phone while waiting for the kiddo. Orgzly Revived and Syncthing let me pick up where I left off.
As I get thoughts down, I can use keyboard shortcuts to rearrange things and then convert the list of sentences into paragraphs. I can easily turn text into links using my favourites.
I can imagine combining the audio clips from the text properties with the videos and screenshots to create a quick narrated clip that I can edit (using Emacs, of course), or publishing the narration so that people can listen to it if they like.
This combination of drawing + audio + text works better for me than any of those components alone. Visuals give me anchors for my thoughts. Audio helps with the braindump. Text permits precise, searchable details we can copy. Like George Jones, I find they all have their uses and their strengths.
When I try to dictate longer sections without that kind of visual support, I often lose my train of thought. I want to see where I've been and where I want to go. I don't trust LLMs to clean up that kind of rough transcript; the output doesn't quite resonate with me. I like keeping speech recognition constrained to sentences I can immediately check or reorganize, and I have the recordings as text properties so I can go back to that segment if I need to (at least during the current editing section). That way, I'm never left scratching my head and wondering what I meant.
I love that local speech recognition is getting better and better. I'm curious about whether small language models can do better turn detection to help compensate for my intra-phrase delays when I'm thinking of a word, and whether decision models can automatically classify a sentence into its place in my outline. I think it would be cool if embeddings or other language models could suggest related words or resources as I go along. Lots of interesting possibilities even with the technology that's already available.
Thanks to George Jones for hosting this month's Emacs Carnival on input alternatives. Check out the page for more posts!
You can e-mail me at sacha@sachachua.com.
-1:-- Emacs Carnival Oct 2026: Exploring keys and voice input in Emacs (Post Sacha Chua)--L0--C0--2026-10-07T12:18:31.000Z
When I'm working with a tutor to improve my French, I sometimes need to push myself to provide more details when I'm sharing and ask more follow-up questions in response to what they're saying, instead of falling back to « D'accord » and « C'est cool. » Visualizing the timeline in real-time lets me see if the turn-taking looks good. I might even need to take a more active role in controlling the conversation if I happen to be meeting a tutor who tends to go on monologues.
When I'm talking out loud on my own for practice, this might also nudge me to reduce the pauses and enrich my vocabulary by practising saying things often enough that they become more automatic.
This code uses this server vad-file.py, which I generated with Google Gemini. It emits JSONL for voice activity detection events (start/stop) and uses Orukeet to transcribe the speech once the voice activity detection notices it's finished. I also use spacy to look up token information because I want to someday colour the nouns by gender, which might allow me to correct myself in the next sentence instead of hesitating in the previous one.
;; Report on the balance between my tutor and me
(defvar sacha-vad-tutor-process nil)
(defvar sacha-vad-tutor-dir "~/vendor/orukeet")
(defvar sacha-vad-tutor-command `(,(expand-file-name ".venv/bin/python" sacha-vad-tutor-dir)
"asr.py"
"--spacy-model"
"fr_dep_news_trf"
"-s" "300" "-t"
"VirtualMicSink.monitor=sacha" "alsa_output.usb-AT_AT2040USB_202011110001-00.analog-stereo.monitor=tutor"))
(defvar sacha-vad-tutor-events nil "List of events as an list of alists (label start stop duration).")
(defvar sacha-vad-tutor-labels '("sacha" "tutor"))
(defvar sacha-vad-tutor-summary '(("sacha" . 0)
("tutor" . 0)))
(defvar sacha-vad-tutor-session-length (* 60 60) "Number of seconds to use for scale.")
(defun sacha-vad-tutor-update-view ()
"Visualize the last 3 minutes and the overview of the session.
Also keep track of target phrases."
(interactive)
(with-current-buffer (get-buffer-create "*tutor stats*")
(erase-buffer)
(let* ((total (apply '+ (mapcar 'cdr sacha-vad-tutor-summary)))
(height 30)
(width (window-width nil t))
(max-seconds
(if sacha-vad-tutor-events
(apply 'max (mapcar (lambda (o) (alist-get 'end o))
sacha-vad-tutor-events))
0))
(threshold 180) ; last three minutes
(threshold-seconds (- max-seconds threshold))
(recent
(seq-keep
(lambda (o)
(when (>= (alist-get 'start o)
threshold-seconds)
(let-alist o
`((label . ,.label)
(start . ,(- .start threshold-seconds))
(end . ,(- .end threshold-seconds))))))
(copy-sequence sacha-vad-tutor-events)))
(recent-total (apply '+ (mapcar (lambda (o)
(-
(alist-get 'end o)
(alist-get 'start o)))
recent))))
(save-excursion
(insert
(format "Last %d minute%s:\n"
(/ threshold 60)
(if (> (/ threshold 60) 1)
"s"
""))
(mapconcat
(lambda (o)
(format "%d%% %s\n"
(/ (* 100.0 (cdr o)) recent-total)
(car o)))
(seq-map
(lambda (group)
(cons (car group)
(apply '+ (mapcar (lambda (o)
(-
(alist-get 'end o)
(alist-get 'start o)))
(cdr group)))))
(seq-group-by (lambda (o) (alist-get 'label o)) recent)))
"\n")
(svg-insert-image
(sacha-vad-visualize
sacha-vad-tutor-labels
recent
t
width height))
(insert
"\nTotal:\n"
(mapconcat
(lambda (o)
(format "%d%% %s\n"
(/ (* 100.0 (cdr o)) total)
(car o)))
sacha-vad-tutor-summary))
(svg-insert-image
(sacha-vad-visualize sacha-vad-tutor-labels sacha-vad-tutor-events t
width height
sacha-vad-tutor-session-length))
(insert "\n\n" (sacha-learn-lang-bingo-summary))))))
(defun sacha-vad-handle-end-event (data)
"Handle the end event."
(let ((event (list
(cons 'label (alist-get 'label data))
(cons 'start (- (alist-get 'timestamp data)
(alist-get 'duration data)
(or sacha-vad-tutor-start-seconds 0)))
(cons 'end (- (alist-get 'timestamp data)
(or sacha-vad-tutor-start-seconds 0))))))
(push event sacha-vad-tutor-events)
(let-alist data
(incf (alist-get (alist-get 'label data) sacha-vad-tutor-summary 0 nil 'string=)
.duration))
(sacha-vad-tutor-update-view)))
(defun sacha-vad-tutor-filter (proc string)
"Add and parse."
(when (buffer-live-p (process-buffer proc))
;; split the string by newline?
(with-current-buffer (process-buffer proc)
(goto-char (point-max))
(save-excursion
(insert string))
(catch 'waiting
(unless (or (eobp) (looking-at "\n*$"))
(let ((data (json-read)))
(when (string= (alist-get 'event data) "end")
(sacha-vad-handle-end-event data))
(when (string= (alist-get 'event data) "final")
(sacha-vad-handle-text-event data))))))))
(defvar sacha-vad-last-speaker nil "Previous speaker.")
(defun sacha-vad-handle-text-event (data)
(with-current-buffer (get-buffer-create "*Transcript*")
(set (make-local-variable 'window-point-insertion-type) t)
(save-excursion
(goto-char (point-max))
;; insert break
(insert
(if (string= sacha-vad-last-speaker (alist-get 'label data))
" "
(concat (if (bobp) "" "\n\n") (alist-get 'label data) ": "))
(alist-get 'text data))
(setq sacha-vad-last-speaker (alist-get 'label data))
(when (string= (alist-get 'label data) "sacha")
(sacha-learn-lang-notice-bingo-words (alist-get 'text data))))))
(defun sacha-vad-tutor-start ()
"Start the process."
(interactive)
(sacha-vad-tutor-ensure)
(setq sacha-vad-tutor-summary '(("sacha" . 0)
("tutor" . 0))
sacha-vad-tutor-events nil)
(with-current-buffer (get-buffer-create "*Transcript*")
(erase-buffer)
(display-buffer (current-buffer)))
(display-buffer (get-buffer-create "*tutor stats*")))
(defun sacha-vad-tutor-ensure ()
"Start the process if it's not already running."
(interactive)
(unless (process-live-p sacha-vad-tutor-process)
(setq sacha-vad-tutor-process-start-time (current-time))
(setq sacha-vad-tutor-start-seconds 0)
(let ((default-directory sacha-vad-tutor-dir))
(setq sacha-vad-tutor-process
(make-process
:name "vad-tutor"
:command sacha-vad-tutor-command
:connection-type 'pipe
:buffer (get-buffer-create "*vad-tutor*")
:stderr (get-buffer-create "*vad-tutor-err*")
:filter #'sacha-vad-tutor-filter)))))
(defun sacha-vad-tutor-stop ()
"Stop the process."
(interactive)
(interrupt-process sacha-vad-tutor-process))
;;;###autoload
(defun sacha-js-parse-jsonl-file (file)
"Parse jsonl as a list"
(let (results)
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(while (not (eobp))
(condition-case nil
(push (json-read) results)
(error (goto-char (point-max))))))
(nreverse results)))
(defvar sacha-vad-tutor-start-seconds 0 "Number of seconds to consider as the start.")
(defvar sacha-vad-tutor-process-start-time nil "Time that the process started.")
;;;###autoload
(defun sacha-vad-tutor-reset-clock ()
"Reset the start of the session."
(interactive)
(setq sacha-vad-tutor-start-seconds
(time-to-seconds (time-subtract (current-time)
sacha-vad-tutor-process-start-time)))
(mapcar (lambda (o) (setcdr o 0)) sacha-vad-tutor-summary)
(setq sacha-vad-tutor-events nil))
(defun sacha-vad-visualize (labels list &optional output-file width height max-seconds)
(setq height (or height 25))
(setq width (or width (window-width nil t)))
(let* ((svg (svg-create width height))
(max-seconds (max
(or max-seconds 0)
(if list
(apply 'max (mapcar (lambda (o) (alist-get 'end o))
list))
0)))
(pixels-per-second (/ (* 1.0 width) max-seconds))
(row-height (/ height (length labels))))
(dolist (o list)
(let-alist o
(let ((y (* row-height (or (seq-position labels .label 'string=) -1))))
(when (>= y 0)
(svg-rectangle
svg
(* .start pixels-per-second)
y
(* (- .end .start) pixels-per-second)
row-height :fill "green")))))
(cond
((stringp output-file)
(with-temp-file output-file
(svg-print svg)))
(output-file
svg)
(t
(with-current-buffer (get-buffer-create "*vad*")
(goto-char (point-min))
(display-buffer (current-buffer))
(erase-buffer)
(svg-insert-image svg))))))
(defun sacha-vad-stats (labels list)
"Return the balance of time talking as (label total percentage)."
(let* ((max-seconds (apply '+ (mapcar
(lambda (o) (let-alist o (- .end .start)))
list)))
(durations (mapcar (lambda (o)
(cons
(car o)
(mapcar (lambda (e) (let-alist e (- .end .start)))
(cdr o))))
(seq-group-by (lambda (o) (alist-get 'label o))
list))))
(seq-map
(lambda (label)
(let* ((row (cdr (assoc label durations)))
(total (apply '+ row)))
`((label . ,label)
(total . ,total)
(count . ,(length row))
(percentage . ,(and total max-seconds (> max-seconds 0) (* 100.0 (/ total max-seconds)))))))
labels)))
;; (sacha-vad-stats '("sacha" "tutor") (seq-take (sacha-js-parse-jsonl-file "~/proj/speech/output2.log") 5))
;; (sacha-vad-stats '("sacha" "tutor") (sacha-js-parse-jsonl-file "~/proj/speech/output.log"))
;; (sacha-vad-visualize '("sacha" "tutor") (sacha-js-parse-jsonl-file "~/proj/speech/output.log"))
;; (sacha-vad-visualize '("sacha" "tutor") (seq-take (sacha-js-parse-jsonl-file "~/proj/speech/output2.log") 30))
(defun sacha-vad-tutor-group-output (files)
(let ((grouped-files (seq-group-by
(lambda (o)
(substring (file-name-base o) 0 10))
files)))
(mapcar
(lambda (group)
(let ((start 0))
(cons (car (cdr group))
(seq-reduce
(lambda (prev file)
(append prev
(let ((max-seconds 0))
(prog1 (mapcar (lambda (event)
(incf (alist-get 'start event) start)
(incf (alist-get 'end event) start)
(setq max-seconds (max (alist-get 'end event) max-seconds))
event)
(sacha-js-parse-jsonl-file file))
(setq start max-seconds)))))
(cdr group)
nil))))
grouped-files)))
(defun sacha-vad-tutor-make-table (files &optional width height)
"Make a summary table."
(let ((labels sacha-vad-tutor-labels)
(width (or width 500))
(height (or height 30)))
;; combine data as needed
(append
'(("Date" "Tutor" "Minutes" "%" "Visualization") hline)
(seq-map
(lambda (group)
(let* ((list (cdr group))
(base (file-name-base (car group))))
(if list
(let-alist (car (sacha-vad-stats labels list))
(let ((svg-name
(expand-file-name
(concat (file-name-base (car group)) "-" .label ".svg")
"~/proj/french/conversation")))
(sacha-vad-visualize
(list .label)
list
svg-name
width
height)
(list
(format-time-string "%Y-%m-%d"
(seconds-to-time
(sacha-filename-timestamp (car group))))
;; (org-link-make-string (concat "file:" o) "file")
(if (string-match "-conversation-\\([^\\.]+\\)" base)
(s-capitalize (substring (match-string 1 base) 0 1))
"")
(round (/ .total 60))
(format "%d%%" .percentage)
(org-link-make-string (concat "file:" svg-name)
(concat "file:" svg-name))
)))
(list "?"))))
(sacha-vad-tutor-group-output files))
nil)))
Here's the code that reminds me to use the words or expressions on my to-do list, and then keeps track of how often I've used them, prioritizes the ones that I haven't used yet, and displays these. It can work with either my Whisper dictation workflow or the VAD events server described above.
I keep the list of expressions in file:///home/sacha/proj/french/bingo.txt.
(defvar sacha-learn-lang-bingo-word-file "~/proj/french/bingo.txt")
(defvar sacha-learn-lang-bingo-words nil "List of words.")
(defvar sacha-learn-lang-bingo-words-hash nil "Data for the counts.")
;;;###autoload
(defun sacha-learn-lang-bingo-words ()
"Load the list of words from the file."
(interactive)
(setq sacha-learn-lang-bingo-words
(seq-map (lambda (o)
(when (= (length o) 1)
(setq o (list (car o) 0)))
(setq o (append o (list
(string-trim (replace-regexp-in-string
" *\\[.+?\\] *" " "
(car o))))))
o)
(pcsv-parse-file sacha-learn-lang-bingo-word-file)))
sacha-learn-lang-bingo-words)
;;;###autoload
(defun sacha-learn-lang-bingo-handler (event)
"Manually register the event."
(interactive "e")
(let* ((pos (posn-point (event-end event)))
(val (get-text-property pos 'bingo)))
(when val
(sacha-learn-lang-notice-bingo-words val))))
(defvar-keymap sacha-learn-lang-bingo-map
"<mouse-1>" #'sacha-learn-lang-bingo-handler)
;;;###autoload
(defun sacha-learn-lang-bingo-summary ()
"Summarize the words to say."
(let ((target (seq-filter (lambda (row) (= (elt row 1) 0))
sacha-learn-lang-bingo-words))
(yay (seq-filter (lambda (row) (> (elt row 1) 0))
sacha-learn-lang-bingo-words)))
(concat
(if target
(concat "Target: "
(mapconcat (lambda (row)
(propertize
(car row)
'keymap sacha-learn-lang-bingo-map
'bingo (car row)))
target
"; ")
"\n\n")
"")
(if yay
(concat
"Yay! "
(mapconcat (lambda (row)
(propertize (format "%s (%s)" (car row) (elt row 1))
'keymap sacha-learn-lang-bingo-map
'bingo (car row)))
(seq-filter (lambda (row) (> (elt row 1) 0))
sacha-learn-lang-bingo-words)
"; ")
"\n\n")
""))))
;;;###autoload
(defun sacha-learn-lang-notice-bingo-words (text)
"Look for the bingo words in TEXT and celebrate when I use them.
Add to `whisper-insert-text-at-point'."
(when text
(unless sacha-learn-lang-bingo-words (sacha-learn-lang-bingo-words))
(let ((found (seq-find
(lambda (o)
(and (elt o 2)
(string-match (concat "\\<" (elt o 2)) text)))
sacha-learn-lang-bingo-words)))
(when found
(incf (elt found 1))
(sacha-vad-tutor-update-view)
(with-current-buffer (get-buffer-create "*Bingo*")
(erase-buffer)
(insert (sacha-learn-lang-bingo-summary))))))
text)
You can e-mail me at sacha@sachachua.com.
-1:-- Using Emacs to visualize speaking and target words/phrases in real-ish time (Post Sacha Chua)--L0--C0--2026-10-07T11:47:04.000Z
Raw link: https://www.youtube.com/watch?v=fGjH82DeF7c
[ The stream will be recorded. You can watch it later. ]
This is a spontaneous live stream. I am working on a new feature for my ‘denote’ Emacs package. The idea is to implement an optional in-memory cache that can help speed up certain operations.
-1:-- Emacs: spontaneous live new ‘denote-data’ feature (2026-10-07 at 18:00 Europe/Athens) (Post Protesilaos)--L0--C0--2026-10-07T00:00:00.000Z
Over at the Dead Parrot BBS, nickb has an excellent post extolling the power of plain text. It’s a notion that’s familiar to all us Emacs users and even more so to Org users. We Org users (or more generally Markdown and other markup users) can write plain text and have it formatted for output in a variety of ways. The point is, the text stays text and is readable even without the formatting.
As nickb says, text written today will still be readable 50 years from now. Sure, UTF-8 and the other Unicode formats can and do add new characters from time to time but text written today will have the same meaning even if new characters are added.
The most important aspect is that you’ll own your data no matter what programs come and go. Emacs isn’t going anywhere but even if it did, it wouldn’t matter. Text will still be readable and something like Org or Markdown will still make intended formatting obvious even if Org and Markdown no longer exist.
Nickb acknowledges that text is not the right answer for everything. You wouldn’t, for example, want to represent a photograph in plain text and there are lots of other examples. Still, for many applications, plain text is not only sufficient but probably the better solution.
-1:-- The Power Of Plain Text (Post Irreal)--L0--C0--2026-10-06T17:53:04.000Z
-1:-- How to Make the Emacs UI Feel More Responsive (Post James Cherti)--L0--C0--2026-10-06T15:38:52.000Z
I just typed
>Closes: #47, #48, #49, #50, #51, #52, #53, #54into a commit description, all by hand.
Is there a way in Emacs to expand some kind of range description to make this easier? 😅
I want to expand strings like "5-10, 15-18, 3" into numbers.
Hmm, could be useful. It reminds me of how page ranges work, which will be handy if I want to tinker with PDFs. Let's see if something like this works:
(defun sacha-parse-page-range (s)
"Parse a page range string and return a list of numbers.
Ex: 1-5
2-4, 6-10
1 4 7-9
3-11/2 - this one specifies an increment"
(seq-mapcat
(lambda (part)
(let ((nums (mapcar 'string-to-number (split-string part " *[-/] *"))))
(if (= (length nums) 1)
nums
(number-sequence (elt nums 0) (elt nums 1) (elt nums 2)))))
(split-string s "[, \t\n\r]+")))
(ert-deftest sacha-parse-page-range ()
(should (equal (sacha-parse-page-range "5-10")
'(5 6 7 8 9 10)))
(should (equal (sacha-parse-page-range "1-5, 8")
'(1 2 3 4 5 8)))
(should (equal (sacha-parse-page-range "1 4 7-9")
'(1 4 7 8 9)))
(should (equal (sacha-parse-page-range "3-11/2")
'(3 5 7 9 11))))
(defun sacha-issue-range (range)
"Expand RANGE to a list of issues and insert them."
(interactive "MRange (ex: 1-5, 7): ")
(let ((s (mapconcat (lambda (n) (format "#%d" n))
(sacha-parse-page-range range)
", ")))
(when (called-interactively-p 'any)
(insert s))
s))
You can e-mail me at sacha@sachachua.com.
-1:-- Emacs Lisp: Parse page ranges, insert issue references (Post Sacha Chua)--L0--C0--2026-10-05T21:30:38.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
The Emacs Carnival for October is about looking into your crystal ball. I'd love to see what you're looking forward to exploring. Thanks to George Jones for hosting!
Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!
You can e-mail me at sacha@sachachua.com.
-1:-- 2026-10-05 Emacs news (Post Sacha Chua)--L0--C0--2026-10-05T14:01:16.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 the 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
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
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
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!