Corwin Brust: Cupcakes
-1:-- Cupcakes (Post Corwin Brust)--L0--C0--2026-03-19T17:59:59.000Z
-1:-- Cupcakes (Post Corwin Brust)--L0--C0--2026-03-19T17:59:59.000Z
Ever found yourself wanting to add a new AI provider to ollama-buddy? (probably not I would guess 🙂), only to realise you’d need to write an entire Elisp module? Or perhaps you’re running a local inference server that speaks the OpenAI API, but can’t be bothered with the ceremony of creating a dedicated provider file?
Fair question. That’s exactly why I built ollama-buddy-provider-create — a single function that lets you register any LLM provider in seconds, whether it’s a cloud API or your own local server.
The traditional approach required a separate .el file for each provider — OpenAI, Claude, Gemini, you name it. Each with its own defcustom variables, configuration boilerplate, and maintenance overhead. It worked, but it felt a bit… heavy-handed for simple use cases.
What if you just wanted to quickly add support for that new local LM Studio instance running on port 1234? Or point ollama-buddy at your company’s internal AI gateway? Previously, you’d be looking at copying an existing provider file and modifying dozens of lines. Now? One function call.
The magic (yes, of the elisp kind!) happens in ollama-buddy-provider.el, which provides a generic provider registration system. Instead of requiring separate Elisp files, you can register any provider with a single call:
(ollama-buddy-provider-create
:name "My Local Server"
:api-type 'openai
:endpoint "http://localhost:1234/v1/chat"
:models-endpoint "http://localhost:1234/v1/models"
:api-key "your-key-here"
:prefix "l:")
Three API types are supported out of the box:
openai (default) — Any OpenAI-compatible chat/completions APIclaude — Anthropic Claude Messages APIgemini — Google Gemini generateContent APIThe system handles all the underlying HTTP requests, error mapping, and session management automatically. Your provider just needs to specify which API flavour it speaks.
Adding a local LM Studio instance:
(ollama-buddy-provider-create
:name "LM Studio"
:api-type 'openai
:endpoint "http://localhost:1234/v1/chat/completions"
:models-endpoint "http://localhost:1234/v1/models"
:api-key "not-needed" ; LM Studio often doesn't require auth
:model-prefix "l:")
Connecting to OpenRouter (400+ models through one API):
(ollama-buddy-provider-create
:name "OpenRouter"
:api-type 'openai
:endpoint "https://openrouter.ai/api/v1/chat/completions"
:models-endpoint "https://openrouter.ai/api/v1/models"
:api-key "your-openrouter-key"
:model-prefix "r:")
After registration, your new provider appears in the status line and becomes available through the standard model selection interface. The model-prefix (like l: for local or r: for OpenRouter) lets you quickly identify which provider a model belongs to.
The provider system leverages ollama-buddy’s shared infrastructure in ollama-buddy-remote.el, which extracts common functionality like request handling, error mapping, and response processing. This means your custom provider gets the same robust error handling as the built-in ones:
When you call ollama-buddy-provider-create, it registers your provider with the core system, making it available to all the usual entry points: the transient menu, model selection, and conversation buffers.
This approach is perfect for:
The beauty ojf this system is that it makes ollama-buddy genuinely extensible without requiring deep knowledge of its internals. Want to add support for that new AI service that launched yesterday? You can probably do it in five lines of configuration rather than fifty.
Next up I think this will be the big one, adding tooling for those external providers!!!
-1:-- Ollama Buddy - Seven Lines to Any LLM Provider (Post James Dyer)--L0--C0--2026-03-19T14:50:00.000Z
That didn’t take long. After yesterday’s post on adapting Bozhidar Batsov’s delete-surrounding-pair function to handle Org markup, Batsov is back with a post on surround.el, a port of surround.vim from Michael Kleehammer. The point of delete-surrounding-pair was to (sort of) emulate Vim’s surround plugin. Now with support.el, there’s no longer a need for delete-surrounding-pair.
Surround does everything delete-surrounding-pair did and a lot more. You can delete surrounding pairs, add pairs, change pairs, mark or kill the content between the pairs, or even kill the text and the surrounding pair. It really is a splendid package and I’ve already added it to my configuration and deleted my adaption of delete-surrounding-pair.
Surround will check if the pair you’re operating on has different opening and closing characters and do the right thing automatically. The other nice thing is that there aren’t a lot of key sequences to remember. Once you pick a prefix that you’ll remember the (single) action keys are mnemonic and easy to remember. In any event, which-key pops up a reminder.
I installed it with the recommended prefix of Meta+’ but keep wanting to invoke it with Hyper+’ so I’ll probably change it but, regardless, I’m already enjoying using it.
Take a look at Batsov’s post or the Github repository for all the details. Batsov’s post has some recommendations on when you should use it and Kleehammer’s repository has information on shortcuts and auto mode, which you should read. This is a really useful package and I recommend that everyone take a look at it.
-1:-- Surround.el (Post Irreal)--L0--C0--2026-03-19T14:18:35.000Z
A hands-on look at using acp2ollama to connect ACP agents to Emacs packages that speak the Ollama API — with results ranging from 'works great' to 'probably don't bother.'
-1:-- ~acp2ollama~ in Emacs for fun and profit (Post Fritz Grabo)--L0--C0--2026-03-19T00:00:00.000Z
The other day I wrote about Bozhidar Batsov’s post on deleting paired delimiters in Emacs and mentioned that he included a bit of Elisp that—sort of—duplicated the behavior of Vim’s surround.vim. The idea is that if you are in a delimited expression and invoke the function, it will ask what delimiter you want to delete and will delete the closest enclosing pair. That obviously has some shortcomings such as not recognizing if one of the delimiters is in, say, a string. Still it’s handy code that does the right thing in most cases.
The code was interesting but I thought I didn’t need it because delete-pair already handled all the cases I was concerned with. As I was writing about Batsov’s post, I had something like =some text= and realized that I didn’t want it displayed in monospace after all. Since delete-pair was on my mind, I tried it but of course it didn’t work because = isn’t a delimiter that delete-pair recognizes so I deleted them by hand.
Later, I realized that I could simply add the Org markup pairs I was interested in to Batsov’s code so I did that and added it to my init.el. It worked perfectly and now I have an easy way of deleting Org markup .
It’s worth noting that Org mode gives you a way to do the opposite too: if you highlight some text and call org-empasize (Ctrl+c Ctrl+x Ctrl+f) it will add the Org markup you choose to the highlighted text.
Note
After I wrote this post, I found out about surround.el. More about that tomorrow.
-1:-- Delete Org Markup Pairs (Post Irreal)--L0--C0--2026-03-18T15:17:09.000Z
A small #Emacs #OrgMode quality-of-life tweak. I often need to replace an org heading's title while preserving the original text in the body. The problem is that pressing enter on a heading inserts a line above the properties drawer, which breaks things.
Here's a function that moves the heading title into the body (below the properties drawer and metadata), and binds it to S-RET:
(defun my-org-demote-title-to-body ()
"Move the current heading's title into the body, below the metadata.
Point returns to the heading for editing."
(interactive)
(org-back-to-heading t)
(let* ((element (org-element-at-point))
(title (org-element-property :raw-value element)))
(org-edit-headline "")
(save-excursion
(org-end-of-meta-data t)
(insert title "\n"))
(org-beginning-of-line)))
(defun my-org-shift-return ()
"On a heading, demote title to body. In a table, copy down."
(interactive)
(cond
((org-at-heading-p) (my-org-demote-title-to-body))
((org-at-table-p) (org-table-copy-down 1))
(t (org-return))))
(define-key org-mode-map (kbd "S-<return>") #'my-org-shift-return)
-1:-- 2026-03-18-001 (Post Srijan Choudhary)--L0--C0--2026-03-18T15:10:00.000Z
It’s been a while since the last super-save release. The last time I wrote about it was back in 2018, when I boldly proclaimed:
It seems that now super-save is beyond perfect, so don’t expect the next release any time soon!
Famous last words. There was a 0.4 release in 2023 (adding a predicate system, buffer exclusions, silent saving, and trailing whitespace cleanup), but I never got around to writing about it. The package has been rock solid for years and I just didn’t pay it much attention – it quietly did its job, which is kind of the whole point of an auto-save package.
The idea behind super-save goes all the way back to a blog post I wrote in
2012
about auto-saving buffers on buffer and window switches. I had been using
IntelliJ IDEA for Java development and loved that it would save your files
automatically whenever the editor lost focus. No manual C-x C-s, no thinking
about it. I wanted the same behavior in Emacs.
Back then, the implementation was crude – defadvice on switch-to-buffer,
other-window, and the windmove commands. That code lived in Emacs
Prelude for a few years before I extracted
it into a standalone package in 2015. super-save was born.
Yesterday I stumbled upon
buffer-guardian.el, a
package with very similar goals to super-save. Its README has a comparison
with
super-save
that highlighted some valid points – mainly that super-save was still relying
on advising specific commands for buffer-switch detection, while newer Emacs
hooks like window-buffer-change-functions and
window-selection-change-functions could do the job more reliably.
The thing is, those hooks didn’t exist when super-save was created, and I
didn’t rush to adopt them while Emacs 27 was still new and I wanted to support
older Emacsen. But it’s 2026 now – Emacs 27.1 is ancient history. Time to
modernize!
This is the biggest super-save release in years! Here are the highlights:
Buffer and window switches are now detected via
window-buffer-change-functions and window-selection-change-functions,
controlled by the new super-save-when-buffer-switched option (enabled by
default). This catches all buffer switches – keyboard commands, mouse clicks,
custom functions – unlike the old approach of advising individual (yet central)
commands.
Frame focus loss is now detected via after-focus-change-function instead of the
obsolete focus-out-hook, controlled by super-save-when-focus-lost (also
enabled by default).
With the new hooks in place, both super-save-triggers and
super-save-hook-triggers now default to nil. You can still use them for edge
cases, but for the vast majority of users, the built-in hooks cover everything.
super-save now knows how to save org-src edit buffers (via
org-edit-src-save) and edit-indirect buffers (via edit-indirect--commit).
Both are enabled by default and controlled by super-save-handle-org-src and
super-save-handle-edit-indirect.
Two new default predicates prevent data loss: verify-visited-file-modtime
avoids overwriting files modified outside Emacs, and a directory existence check
prevents errors when a file’s parent directory has been removed. Predicate
evaluation is also wrapped in condition-case now, so a broken custom predicate
logs a warning instead of silently disabling all auto-saving.
This allowed cleaning up the code and relying on modern APIs.
For most users, upgrading is seamless – the new defaults just work. If you had
a custom super-save-triggers list for buffer-switching commands, you can
probably remove it entirely:
;; Before: manually listing every command that switches buffers
(setq super-save-triggers
'(switch-to-buffer other-window windmove-up windmove-down
windmove-left windmove-right next-buffer previous-buffer))
;; After: the window-system hooks catch all of these automatically
;; Just delete the above and use the defaults!
If you need to add triggers for commands that don’t involve a buffer switch
(like ace-window), super-save-triggers is still available for that.
A clean 0.5 setup looks something like this:
(use-package super-save
:ensure t
:config
;; Save buffers automatically when Emacs is idle
(setq super-save-auto-save-when-idle t)
;; Don't display "Wrote file..." messages in the echo area
(setq super-save-silent t)
;; Disable the built-in auto-save (backup files) since super-save handles it
(setq auto-save-default nil)
(super-save-mode +1))
It’s also worth noting that Emacs 26.1 introduced auto-save-visited-mode,
which saves file-visiting buffers to their actual files after an idle delay.
This overlaps with super-save-auto-save-when-idle, so if you prefer using
the built-in for idle saves, you can combine the two:
(use-package super-save
:ensure t
:config
;; Don't display "Wrote file..." messages in the echo area
(setq super-save-silent t)
;; Disable the built-in auto-save (backup files)
(setq auto-save-default nil)
(super-save-mode +1))
;; Let the built-in auto-save-visited-mode handle idle saves
(auto-save-visited-mode +1)
Most of my Emacs packages are a fine example of what I like to call
burst-driven
development
– long periods of stability punctuated by short intense bursts of activity. I
hadn’t touched super-save in years, then spent a few hours modernizing the
internals, adding a test suite, improving the documentation, and cutting a
release. It was fun to revisit the package after all this time and bring it up to
2026 standards.
If you’ve been using super-save, update to 0.5 and enjoy the improvements. If
you haven’t tried it yet – give it a shot. Your poor fingers might thanks for you this,
as pressing C-x C-s non-stop is hard work!
That’s all I have for you today. Keep hacking!
-1:-- super-save 0.5: Modernized and Better Than Ever (Post Emacs Redux)--L0--C0--2026-03-18T09:00:00.000Z
Everyone knows I love finding whatever Emacs tricks and dodges I can to help writers. I wrote an article a while back about the benefits of writing in one-sentence-per-line (OSPL) style. No need to re-hash the whole article, but suffice to say this style can help you organize and transpose sentences, get a quick visual cue for sentence length and complexity, as well as clearer version control diffs, etc.
Transitioning over to this style from the standard paragraph-per-line style can be a little disorienting at first, especially if your longer soft-wrapped sentences are starting to ramble into two or three lines. This where hl-line-mode in Emacs can be a big help. Most text editors have a highlighting feature to help you focus on the current working line, and Emacs is no different. In this article, we’ll take a look at the built-in highlighting mode in Emacs and all the hows and whys you need to know.
Before we get into it, I will mention that if you are a power writer, and want to join the elite of Emacs users, you should check out my downloadable DRM-free eBooks here:
Let’s dig into hl-line-mode now.
hl-line-mode is a built-in Emacs minor mode that highlights the line containing your point. That basically means the line where your cursor is sitting will be highlighted. Different themes will have different highlight colors that fit nicely within the current palette, so no need to worry about getting an ugly yellow caution line in your Emacs.
The mode has been part of Emacs since version 21, so it is already installed in your editor. No additional packages required.
The author Henry James (1843-1916) is famous for writing egregiously long sentences extended with prepositional phrases, nested clauses, and deferred verbs. Contemporary writers shun these kinds of sentence steroids because they can make lines difficult for readers to parse. Nevertheless, it’s a strong style that can be employed strategically, particularly in sections that are meant to pull the reader’s focus in or create disorienting special effects. On the flip side, done well, longer sentences can lull readers through luxurious passages that are a pleasure to write and read. So, if you want to use this style, hl-line-mode paired with OSPL can be a secret weapon in your text editor arsenal.
With highlighting enabled, you get a whisper-subtle visual indicator of your position in the buffer, making it easier to track where you are in a large document, but also helping you focus on a single line.
Beyond OSPL, the mode benefits anyone working with large files. Writers can visually anchor themselves in a long draft while programmers get a persistent cue for the current line in dense code, which pairs nicely with line numbers. Whether writing or programming, this mode can also help you avoid unwanted line breaks.
To enable hl-line-mode, you can add one line to your Emacs config:
(global-hl-line-mode 1)
This activates highlighting in every buffer.
If you only want to try it interactively in the current session, run:
M-x global-hl-line-mode
You can also enable it per-buffer with M-x hl-line-mode if you prefer selective use.
As I mentioned above, I like writing in OSPL style, so having highlighting enabled, particularly when working on some long Henry James type sentences it can really help me isolate my focus without getting distracted by all the text surrounding that sentence.
The post Writing gloriously long sentences in Emacs with line highlighting appeared first on Chris Maiorana.
-1:-- Writing gloriously long sentences in Emacs with line highlighting (Post Chris Maiorana)--L0--C0--2026-03-18T04:00:46.000Z
Today, I was going to work on a python project for half an hour at the end of my work day. I tried to find a good python module to use to work with RSS and Atom feeds. I found one and installed it in my virtual environment and pasted in some example code in a buffer in Emacs. I wanted to run the code in the python shell with Emacs' handy C-c C-c keyboard shortcut. So I opened the shell with C-c C-p and then hit C-c C-c, but I got an error that the shell did not understand the import on line 1 where I was importing the module I just installed in my virtualenv. I realised the shell ran from outside the virtualenv. I had not looked into this before, but found there were various packages people recommended to fix this. I tried one that looked interesting, but it did not work. Maybe because I was on my work laptop with Windows at the time. I tried a few things, but soon gave up.
I then discovered .dir-locals.el was a possible solution. It gives buffer-local values to variables inside the folder it is placed for the mode that you configure through an alist. The first suggestion I found was to use a variable that did not work at all. I wasted a lot of time trying to get it to work. When I was close to giving up, I discovered that I could use the usual python-shell-interpreter variable in the .dir-locals.el file and it worked. I then thought that the solution I had found would not work on my own machines that run GNU/Linux since the python.exe-file neither exists nor the folder it is in in virtual environments on my preferred platform. A .dir-locals.el-file checked in with git is not a particularly cross-platform solution. I thought I could make a function in my config that would check which platform I was on, whether I was in a project or not and whether a directory with the name venv exists (I tend to use that name) and then just set the variable to the correct path based on that.
When I got home, after dinner, I made that function. I love how well-documented Emacs is. It makes doing things like this really easy. I just looked up function names starting with project and found something useful after reading up on a few. A bit of evaluation in different buffers with C-x C-e to check if things worked the way I thought and a bit of tweaking when it turned out that cddr did not return the same as nth 2 (cddr returned a list with a string, but nth 2 returned a string) and then I ended up with this function:
(defun emo-python-virtualenv () "Sets the python interpreter to python in the venv if in a project and a venv exists." (when (project-current) (let ((pythonpath (concat (nth 2 (project-current)) (if (eq system-type 'gnu/linux) "venv/bin/python" "venv/Scripts/python.exe")))) (when (file-exists-p pythonpath) (setq-local python-shell-interpreter pythonpath)))))
To put it to work every time I open a python file, I also needed to add it to python-mode-hook like this in my configuration for Python mode:
(add-hook 'python-mode-hook 'emo-python-virtualenv)
When I now try to import packages that only exists in a venv into the python shell, it works if I loaded the shell from a file within the same project as the venv with the package, but not from a file from outside that project. If I load the shell from a file within a project without a venv, I get the system's Python and its available packages. No need for outdated packages that doesn't work or directory-local variables that are platform dependent. This should work on Windows as well. (I haven't tested it yet, but I will tomorrow.) It's easy and fun to fix things like this in Emacs, and I learned some Elisp and a bit about Python virtual environments in the process. It would be convenient if things like these worked out of the box, but that would demand a bit more code since my function works on the basis of me using the name venv for every venv which other people might not.
-1:-- Use virtual environment in Emacs' Python Mode if in a project with a venv (Post Einar Mostad)--L0--C0--2026-03-17T17:16:00.000Z
A recurrent theme here at Irreal is that Emacs is for everyone, not just programmers. Every time I find an example of this I try to write about it. It’s a point that Protesilaos Stavrou made in his recent FOSS @ Oxford talk.
Now, in a lovely piece of serendipity, we have Randy Ridenour providing a real world example. Ridenour is a Philosophy professor and, as he himself says, is by no means a programmer. Still, as Prot suggested, he has learned enough to automate some of his Emacs tasks. He started by copying other people’s code but tried to understand what it did.
Now he needs to convert quiz questions from one format to another for the learning management system that his university uses—see his post for the details. This amounts to running a series of regular expression replacements, which he did by hand for a while. Then he realized—as I always find myself doing—that he could just write a bit of Elisp to automate the process.
Ridenour isn’t a programmer and he isn’t a young turk who grew up with all this technology. He’s old enough to have a married daughter so while he may or may not qualify for the Irreal Geezer Club, he is obviously well along in his career. And yet he finds it easy to learn enough Elisp to reduce the friction in his workflow. His colleagues are probably still grumbling about how hard it is to deal with the quizzes but he’s turned all those problems over to Emacs.
-1:-- Non Programmer Use Of Emacs (Post Irreal)--L0--C0--2026-03-17T15:04:15.000Z
If you maintain a tree-sitter major mode that has a REPL (comint) companion, you’ve probably wondered: can the REPL input get the same syntax highlighting and indentation as source buffers? The answer is yes – and the infrastructure has been in Emacs since 29.1. It’s just not widely known yet.
I ran into this while working on neocaml,
my tree-sitter major mode for OCaml. The OCaml REPL buffer used simple
regex-based font-lock-keywords for input, which was definitely a step backward from
the rich highlighting in source buffers.
Initially I didn’t even bother to research using tree-sitter font-lock in
comint, as assumed that would be something quite complicated. Turns out the fix
was surprisingly easy.
comint-fontify-input-modeEmacs 29.1 introduced comint-fontify-input-mode, a minor mode that fontifies
input regions in comint buffers through an indirect buffer. The idea is
simple:
comint-indirect-setup-function.Here’s all it took for neocaml’s REPL:
(define-derived-mode neocaml-repl-mode comint-mode "OCaml-REPL"
;; ... existing setup ...
;; Tree-sitter fontification for REPL input
(setq-local comint-indirect-setup-function #'neocaml-mode)
(comint-fontify-input-mode))
That’s it. REPL input now gets the exact same tree-sitter font-lock as .ml
buffers. The existing font-lock-keywords for output (error messages, warnings,
val/type results) keep working as before – they only apply to output
regions.
Important: comint-fontify-input-mode is incompatible with
comint-use-prompt-regexp – the two features can’t be active at the same
time. Most modern comint-derived modes don’t set comint-use-prompt-regexp to
t, so this usually isn’t an issue.
Two built-in modes use this approach:
sh-mode in the indirect buffer (enabled by default via
shell-fontify-input-enable)emacs-lisp-mode (enabled by default via
ielm-fontify-input-enable)On the third-party side, inf-lua and
ts-repl both use this pattern with
tree-sitter modes. And now there’s also neocaml, of course. :-)
comint-indent-input-line-defaultComint also provides indentation delegation via comint-indent-input-line-default
and comint-indent-input-region-default. When you set these as
indent-line-function and indent-region-function, pressing TAB on an input
line delegates indentation to the indirect buffer’s indent-line-function –
which will be treesit-indent if the indirect buffer runs a tree-sitter mode.
(setq-local indent-line-function #'comint-indent-input-line-default)
(setq-local indent-region-function #'comint-indent-input-region-default)
shell.el already does this. For your own REPL modes, you can add these two lines alongside the font-lock setup.
Here’s where things get tricky. Tree-sitter parsers are shared between
indirect and base buffers (this is by design – see
bug#59693). When
treesit-indent runs in the indirect buffer, the parser it uses sees the
entire comint buffer – prompts, output, previous commands, everything. The
parse tree will be full of errors from non-code content.
Font-lock handles this gracefully because comint-fontify-input-mode only
applies fontification results to input regions, so garbled parses of output
regions are harmless. But indentation is different – treesit-indent looks at
the node context around point, and a broken parse tree can confuse it.
In practice, it works better than you’d expect.1 For simple multi-line expressions, the local tree-sitter nodes at the cursor position are often correct enough for reasonable indentation. But for deeply nested multi-line input, the results can be off.
Because of this, I chose not to enable indentation delegation by default in neocaml’s REPL. Instead, it’s documented as an opt-in configuration for adventurous users.
If you maintain a tree-sitter mode with a comint REPL, here’s the minimal pattern:
(define-derived-mode my-repl-mode comint-mode "My-REPL"
;; ... your existing setup ...
;; Font-lock: full tree-sitter highlighting for input
(setq-local comint-indirect-setup-function #'my-ts-mode)
(comint-fontify-input-mode)
;; Indentation: delegate to tree-sitter (experimental)
(setq-local indent-line-function #'comint-indent-input-line-default)
(setq-local indent-region-function #'comint-indent-input-region-default))
Consider making these features opt-in via defcustoms, especially the indentation
part. And remember that your existing font-lock-keywords for output
highlighting (errors, warnings, result values) will continue working – they
don’t conflict with comint-fontify-input-mode.
That’s it. Two overlooked comint features, a few lines of setup, and your REPL goes from basic regex highlighting to full tree-sitter support.
Keep hacking!
Depends on your expectations, of course. ↩
-1:-- Tree-sitter Font-Lock and Indentation in Comint Buffers (Post Emacs Redux)--L0--C0--2026-03-17T14:30:00.000Z
In my recent article on removing paired delimiters, I mentioned that I kind of
miss Vim’s surround.vim experience in
Emacs. Well, it turns out someone has done something about it –
surround.el brings the core ideas of
surround.vim to native Emacs, without requiring Evil mode.
surround.vim (and similar plugins like
mini.surround in Neovim) are
some of my favorite Vim packages. The idea is so simple and so useful that it
feels like it should be a built-in. So I’m happy to see someone took the time to
port that beautiful idea to Emacs.
For those who haven’t used surround.vim, the concept is straightforward. You
have a small set of operations for working with surrounding characters –
the delimiters that wrap some piece of text:
ds( removes the parentheses around pointcs(" changes parens to quotesys + motion + character wraps text with a delimiterThat’s basically it. Three operations, consistent keybindings, works everywhere regardless of file type. The beauty is in the uniformity – you don’t need different commands for different delimiters, and you don’t need to think about which mode you’re in.
surround.el is available on MELPA and the setup is minimal:
(use-package surround
:ensure t
:bind-keymap ("M-'" . surround-keymap))
You bind a single keystroke (M-' in this example) to surround-keymap, and that
gives you access to all the operations through a second keystroke. Here are the
commands available in the keymap:
| Key | Operation |
|---|---|
s |
Surround region/symbol at point |
d |
Delete surrounding pair |
c |
Change pair to another |
k |
Kill text inside pair |
K |
Kill text including pair |
i |
Mark (select) inside pair |
o |
Mark including pair |
There are also shortcuts for individual characters – pressing an opening
delimiter (like () after the prefix does a mark-inner, while pressing the
closing delimiter (like )) does a mark-outer.
Let’s see how this works in practice. Starting with the word Hello (with point
somewhere on it):
Surround – M-' s " wraps the symbol at point with quotes:
Hello → "Hello"
Change – M-' c " ( changes the surrounding quotes to parens:
"Hello" → (Hello)
Mark inner – M-' i ( selects just the text inside the parens:
(|Hello|) ;; "Hello" is selected
Mark outer – M-' o ( (or M-' )) selects the parens too:
|( Hello)| ;; "(Hello)" is selected
Delete – M-' d ( removes the surrounding parens:
(Hello) → Hello
Kill – M-' k ( kills the text inside the pair (leaving the delimiters
gone too), while M-' K ( kills everything including the delimiters.
Like surround.vim, surround.el distinguishes between “inner” (just the
content between delimiters) and “outer” (content plus the delimiters themselves).
The i and k commands operate on inner text, o and K on outer.
There’s also an “auto” mode – the default for i and k – that behaves as
inner when you type an opening character and outer when you type a closing
character. So M-' ( marks inner, M-' ) marks outer. Handy shortcut if your
fingers remember it (I’m still building the muscle memory).
One caveat: auto mode can’t distinguish inner from outer for symmetric pairs
like quotes (", '), since the opening and closing character are the same. In
those cases it defaults to inner.
The biggest difference is in how you surround text. In Vim, surround.vim
uses motions – ysiw( means “surround inner word with parens.” In Emacs,
surround.el operates on the active region or the symbol at point. So the
typical workflow is: select something, then M-' s (.
This actually pairs beautifully with
expreg (which I wrote about
recently). Use expreg
to incrementally select exactly the text you want, then M-' s to wrap it. It’s
a different rhythm than Vim’s motion-based approach, but once you get used to
it, it feels natural.
The other operations (d, c, k, i, o) work similarly to their Vim
counterparts – you invoke the command and then specify which delimiter you’re
targeting.
surround.el fills a specific niche:
Using electric-pair-mode? Then surround.el is an excellent complement.
electric-pair-mode handles auto-pairing when you type delimiters, but
offers nothing for removing, changing, or wrapping existing text with
delimiters. surround.el fills exactly that gap.
Using smartparens? You probably don’t need surround.el –
smartparens already has sp-unwrap-sexp, sp-rewrap-sexp, and friends. The
overlap is significant, and adding another package on top would just be
confusing.
Using paredit? Same story for Lisp code – paredit has you covered
with paredit-splice-sexp, paredit-wrap-round, and so on. But if you want
the surround experience in non-Lisp buffers, surround.el is a good pick.
My current setup is paredit for Lisps, electric-pair-mode for everything
else, and I’m adding surround.el to complement the latter. Early days, but it
feels right.
If you’ve ever wondered whether some Vim feature you miss exists in Emacs –
the answer is always yes. It’s Emacs. Of course someone has written a package
for it. Probably several, in fact… Admittedly, I discovered surround.el only
when I had decided to port surround.vim to Emacs myself. :D
That’s all I have for you today. Keep hacking!
-1:-- surround.el: Vim-Style Pair Editing Comes to Emacs (Post Emacs Redux)--L0--C0--2026-03-17T08:00:00.000Z
The buffer-guardian Emacs package provides buffer-guardian-mode, a global mode that automatically saves buffers without requiring manual intervention.
By default, buffer-guardian-mode saves file-visiting buffers when:
In addition to regular file-visiting buffers, buffer-guardian-mode also handles specialized editing buffers used for inline code blocks, such as org-src (for Org mode) and edit-indirect (commonly used for Markdown source code blocks). These temporary buffers are linked to an underlying parent buffer. Automatically saving them ensures that modifications made within these isolated code environments are correctly propagated back to the original Org or Markdown file.
If this package enhances your workflow, please show your support by
starring buffer-guardian on GitHub to help more users discover its benefits.
Other features that are disabled by default:
buffer-guardian-save-on-same-buffer-window-change)buffer-guardian-save-all-buffers-interval)buffer-guardian-save-all-buffers-idle)buffer-guardian-inhibit-saving-remote-files)buffer-guardian-inhibit-saving-nonexistent-files)buffer-guardian-max-buffer-size)buffer-guardian-exclude-regexps)buffer-guardian-predicate-functions)(Buffer Guardian runs in the background without interrupting the workflow. For example, the package safely aborts the auto-save process if the file is read-only, if the file’s parent directory does not exist, or if the file was modified externally. Additionally, it gracefully catches and logs errors if a third-party hook attempts to request user input, ensuring that the editor never freezes during an automatic background save.)
To install buffer-guardian from MELPA:
(use-package buffer-guardian
:custom
;; When non-nil, include remote files in the auto-save process
(buffer-guardian-inhibit-saving-remote-files t)
;; When non-nil, buffers visiting nonexistent files are not saved
(buffer-guardian-inhibit-saving-nonexistent-files nil)
;; Save the buffer even if the window change results in the same buffer
(buffer-guardian-save-on-same-buffer-window-change t)
;; Non-nil to enable verbose mode to log when a buffer is automatically saved
(buffer-guardian-verbose nil)
;; Save all buffers after N seconds of user idle time. (Disabled by default)
;; (buffer-guardian-save-all-buffers-idle 30)
:hook
(after-init . buffer-guardian-mode))
Here is how to install buffer-guardian on Doom Emacs:
~/.doom.d/packages.el file:(package! buffer-guardian
:recipe
(:host github :repo "jamescherti/buffer-guardian.el"))
~/.doom.d/config.el:(after! buffer-guardian
;; When non-nil, include remote files in the auto-save process
(setq buffer-guardian-inhibit-saving-remote-files t)
;; When non-nil, buffers visiting nonexistent files are not saved
(setq buffer-guardian-inhibit-saving-nonexistent-files nil)
;; Save the buffer even if the window change results in the same buffer
(setq buffer-guardian-save-on-same-buffer-window-change t)
;; Non-nil to enable verbose mode to log when a buffer is automatically saved
(setq buffer-guardian-verbose nil)
;; Save all buffers after N seconds of user idle time. (Disabled by default)
;; (setq buffer-guardian-save-all-buffers-idle 30)
(buffer-guardian-mode 1))
doom sync command:doom sync
You can customize buffer-guardian to fit your workflow. Below are the main customization variables:
buffer-guardian-save-on-focus-loss (Default: t): Save when the Emacs frame loses focus.buffer-guardian-save-on-minibuffer-setup (Default: t): Save when the minibuffer opens.buffer-guardian-save-on-buffer-switch (Default: t): Save when window-buffer-change-functions runs.buffer-guardian-save-on-window-selection-change (Default: t): Save when window-selection-change-functions runs.buffer-guardian-save-on-window-configuration-change (Default: t): Save when window-configuration-change-hook runs.buffer-guardian-save-on-same-buffer-window-change (Default: nil): Save the buffer even if the window change results in the same buffer.buffer-guardian-save-all-buffers-interval (Default: nil): Save all buffers periodically every N seconds.buffer-guardian-save-all-buffers-idle (Default: nil): Save all buffers after N seconds of user idle time.buffer-guardian-inhibit-saving-remote-files (Default: t): Prevent auto-saving remote files.buffer-guardian-inhibit-saving-nonexistent-files (Default: t): Prevent saving files that do not exist on disk.buffer-guardian-exclude-regexps (Default: nil): A list of regular expressions for file names to ignore.buffer-guardian-max-buffer-size (Default: nil): Maximum buffer size (in characters) to save. Set to 0 or nil to disable.buffer-guardian-predicate-functions (Default: nil): List of predicate functions to determine if a buffer should be saved.buffer-guardian-handle-org-src (Default: t): Enable automatic saving for org-src buffers.buffer-guardian-handle-edit-indirect (Default: t): Enable automatic saving for edit-indirect buffers.buffer-guardian-save-all-buffers-trigger-hooks: A list of hooks that trigger saving all modified buffers. Defaults to nil.buffer-guardian-save-trigger-functions: A list of functions to advise. A :before advice will save the current buffer before these functions execute.buffer-guardian-verbose (Default: nil): Enable logging messages when a buffer is saved.The buffer-guardian Emacs package has been written by James Cherti and is distributed under terms of the GNU General Public License version 3, or, at your choice, any later version.
Copyright (C) 2026 James Cherti
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program.
Other Emacs packages by the same author:
outline-mode, outline-minor-mode, outline-indent-minor-mode, org-mode, markdown-mode, vdiff-mode, vdiff-3way-mode, hs-minor-mode, hide-ifdef-mode, origami-mode, yafolding-mode, folding-mode, and treesit-fold-mode. With Kirigami, folding key bindings only need to be configured once. After that, the same keys work consistently across all supported major and minor modes, providing a unified and predictable folding experience.-1:-- buffer-guardian.el – Automatically Save Emacs Buffers Without Manual Intervention (When Buffers Lose Focus, Regularly, or After Emacs is Idle) (Post James Cherti)--L0--C0--2026-03-16T21:57:02.000Z
Security reminder: If you use kubernetes-el, don't update for now, and you might want to check your installation if you updated it recently. The repo was compromised. (Analysis, Reddit discussion, lobste.rs) If you use Emacs 31, please consider enabling package-review-policy.
There were a number of lively conversations around Emacs Solo (142 comments on HN), Emacs and Vim in the age of AI (52 comments on Reddit, 138 on HN), and agent-shell 0.47 (62 on Reddit). Also, Prot has posted the video and text of his talk Computing in freedom with GNU Emacs (YouTube 42:40, Video with Q&A, more links in the community section).
Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!
You can comment on Mastodon or e-mail me at sacha@sachachua.com.
-1:-- 2026-03-16 Emacs news (Post Sacha Chua)--L0--C0--2026-03-16T16:40:12.000Z
A perennial problem for Emacs users are the yes/no prompts that require answering “yes” or “no” instead of just “y” or “n”. The usual solution—adopted by almost everyone—is to alias yes-or-no-p to y-or-n-p. Sometime around Emacs 28 or 29 the new variable use-short-answers was introduced as another way of solving the problem.
The last time I wrote about solving the yes-or-no-p problem, I said,
I’ve had my original aliasing code for at least 17 years and see no reason to change it. When use-short-answers was first introduced, I thought I should adopt it but I never did and as far as I can see, it doesn’t make any difference.
That statement was wrong in two ways. First, as related by Bozhidar Batsov, the alias trick doesn’t always work with native compilation because the call to yes-or-no-p may have been compiled before the alias is made. For that reason, use-short-answers is the better solution because it always work.
The second way it was wrong is that when I went to add use-short-answers to my init.el, I discovered that I had already done so, probably back in Emacs 29.
-1:-- Why You Should Use use-short-answers (Post Irreal)--L0--C0--2026-03-16T14:01:16.000Z
Yet another neocaml issue taught me
something I didn’t know about Emacs. Someone
requested that the docs show
how to customize font-lock faces per mode rather than globally. The suggested
approach used face-remap-add-relative – a function I’d never heard of, despite
20+ years of daily Emacs use. Time to dig in.
Let’s say you want type names in OCaml buffers to be displayed in a different
color than in, say, Python buffers. The naive approach is to use custom-set-faces
or set-face-attribute:
(set-face-attribute 'font-lock-type-face nil :foreground "DarkGreen")
This works, but it’s a global change – every buffer that uses
font-lock-type-face will now show types in dark green.
To be fair, global face customization is perfectly valid in several scenarios:
Your theme doesn’t cover certain faces. Many themes only style a subset of
the faces that various packages define. If a face looks wrong or unstyled, a
global set-face-attribute or custom-set-faces is the quickest fix.
Your theme makes choices you disagree with. I recently ran into this with
the Catppuccin theme – the styling of
font-lock-variable-name-face didn’t match how I expected it to look, so I
filed a PR. It got closed
because different modes interpret that face inconsistently (definitions in
Elisp, references in C), making a universal fix impractical for the theme.
That’s exactly the kind of situation where you’d want to override the face
yourself.
You want consistent styling everywhere. If you just want all comments to be italic, or all strings to use a specific color regardless of mode, global is the way to go. No need to complicate things with per-buffer remapping.
The problem comes when you bounce between several languages (and let’s be
honest, most of us do) and you want different visual treatment depending
on the mode. Not all modes use the built-in font-lock faces consistently, and for
some – especially markup languages – there’s a lot of room for improvisation
in how faces get applied. A global change to font-lock-keyword-face might
look great in your Python buffers but terrible in your Org files.
That’s where buffer-local face remapping comes in.
face-remap-add-relativeface-remap-add-relative has been around since Emacs 23 (it lives in
face-remap.el), and it does exactly what the name suggests – it remaps a face
relative to its current definition, and only in the current buffer. The change
is buffer-local, so it won’t leak into other buffers.
Here’s the basic usage:
(face-remap-add-relative 'font-lock-type-face :foreground "DarkGreen")
To apply this automatically in a specific mode, hook it up:
(defun my-ocaml-faces ()
"Customize faces for OCaml buffers."
(face-remap-add-relative 'font-lock-type-face :foreground "DarkGreen")
(face-remap-add-relative 'font-lock-function-name-face :weight 'bold))
(add-hook 'neocaml-mode-hook #'my-ocaml-faces)
Now OCaml buffers get their own face tweaks while everything else stays untouched. You can do the same for any mode – just swap the hook and adjust the faces to taste.
face-remap-add-relative returns a cookie – a token you’ll need if you want
to undo the remapping later. If you’re just setting things up in a mode hook
and leaving them, you can ignore the cookie. But if you want to toggle the
remapping on and off, you’ll need to hold onto it:
(defvar-local my-type-face-cookie nil
"Cookie for type face remapping.")
(defun my-toggle-type-face ()
"Toggle custom type face in current buffer."
(interactive)
(if my-type-face-cookie
(progn
(face-remap-remove-relative my-type-face-cookie)
(setq my-type-face-cookie nil)
(message "Type face remapping removed"))
(setq my-type-face-cookie
(face-remap-add-relative 'font-lock-type-face
:foreground "DarkGreen"))
(message "Type face remapping applied")))
Note the use of defvar-local – since face remapping is buffer-local, your
cookie variable should be too.
A cleaner approach is to wrap this in a minor mode:
(defvar-local my-type-remap-cookie nil)
(define-minor-mode my-type-remap-mode
"Minor mode to remap type face in current buffer."
:lighter " TypeRemap"
(if my-type-remap-mode
(setq my-type-remap-cookie
(face-remap-add-relative 'font-lock-type-face
:foreground "DarkGreen"))
(when my-type-remap-cookie
(face-remap-remove-relative my-type-remap-cookie)
(setq my-type-remap-cookie nil))))
A few more things worth knowing in this area:
face-remap-set-base – sets the base remapping for a face in the current
buffer. Unlike face-remap-add-relative (which layers on top of the existing
face), this replaces the face definition entirely for that buffer. Use this
when you want to completely override a face rather than tweak it.
buffer-face-mode / buffer-face-set – a built-in minor mode that
remaps the default face in the current buffer. This is what powers
M-x buffer-face-set and is handy if you want a different base font in
specific buffers (say, a proportional font for prose and a monospace font
for code).
text-scale-adjust (C-x C-= / C-x C--) – the familiar text scaling
commands actually use face-remap-add-relative under the hood to remap the
default face. So if you’ve ever zoomed text in a single buffer, you’ve been
using face remapping without knowing it.
face-remapping-alist – the buffer-local variable where all of this state
is stored. You generally shouldn’t manipulate it directly (that’s what the
functions above are for), but it’s useful for debugging – check its value in
a buffer to see what remappings are active.
I have to admit – I’m a bit embarrassed that face-remap-add-relative has been
sitting in Emacs since version 23 and I’d never once used it. Probably because
I never felt the need for per-mode face customizations – but I can certainly see
why others would, especially when working across languages with very different
syntax highlighting conventions.
Working on neocaml has been a gold mine of learning (and relearning). I’m happy to keep sharing the things I discover along the way. Keep hacking!
-1:-- Buffer-Local Face Remapping with face-remap-add-relative (Post Emacs Redux)--L0--C0--2026-03-16T06:30:00.000Z
Back in January I announced eglot-python-preset, an Emacs package that simplifies Python LSP configuration with Eglot. At the time, I mentioned rass support as a future direction. That future has arrived, and it brought a companion package along with it.
Both eglot-python-preset↗ and the new eglot-typescript-preset↗ now support rassumfrassum↗ (rass), an LSP multiplexer written by Eglot’s author. This means you can run multiple language servers simultaneously through a single Eglot connection, such as a type checker alongside a linter or a language server alongside a formatter, with minimal configuration.
The eglot-typescript-preset package covers Astro, Vue, Svelte, Deno, and CSS/Tailwind out of the box. On the linting side, it integrates with ESLint, oxlint, Biome, and oxfmt through rass, so you can combine a type checker with one or more linters in the same Eglot session.
This is a beta release. The rass backend works well in my day-to-day use,
but the MELPA packages don’t support rass yet. I have pending PRs with MELPA for
both packages, and eglot-typescript-preset isn’t on MELPA at all yet. For now,
manual installation (cloning the repos) is the way to go if you want to try rass
support.
Eglot is designed around one language server per major mode. If you want type checking from ty and linting from Ruff in the same Python buffer, or TypeScript diagnostics alongside ESLint, you’d normally have to choose one or run a separate tool outside the LSP workflow.
rass solves this by acting as a stdio multiplexer: it presents itself as a single LSP server to Eglot while dispatching requests to multiple backend tools. From Eglot’s perspective, nothing changes. From your perspective, you get diagnostics, completions, and code actions from all your tools in one place.
The preset packages handle the rass configuration for you. You declare which
tools you want, and the package generates the appropriate rass preset file
automatically. When your project has tools installed locally (in .venv or
node_modules), those executables are preferred over global installations.
To use rass with eglot-python-preset, set the LSP server to rass and list
your tools:
(use-package eglot-python-preset
:ensure t
:after eglot
:custom
(eglot-python-preset-lsp-server 'rass)
(eglot-python-preset-rass-tools '(ty ruff))
:config
(eglot-python-preset-setup))
This gives you ty’s type checking and Ruff’s linting diagnostics in every Python buffer, with zero per-project setup for standard projects. PEP-723 scripts work too: the generated rass preset includes the script’s cached uv environment so both tools resolve imports correctly.
You can also use basedpyright instead of ty, or pass literal command vectors for custom tools:
(setopt eglot-python-preset-rass-tools
'(basedpyright
ruff
["custom-lsp" "--stdio"]))
eglot-typescript-preset is brand new. It configures Eglot for TypeScript,
JavaScript, CSS, and several frontend frameworks, with rass support for
combining tools like ESLint, Biome, oxlint, and oxfmt alongside the TypeScript
language server.
The default setup requires no rass and works out of the box:
(use-package eglot-typescript-preset
:ensure t
:after eglot
:config
(eglot-typescript-preset-setup))
This configures typescript-language-server for TS/JS files, astro-ls for
Astro files, and vscode-css-language-server with tailwindcss-language-server
for CSS, all automatically.
One setup I’m particularly happy with is a fully-working Astro project with both ESLint and oxlint. The idea is that oxlint handles the fast, low-level lint rules while ESLint covers the Astro-specific and framework-aware rules. To avoid duplicate diagnostics, the eslint-plugin-oxlint↗ package disables ESLint rules that oxlint already covers.
On the Emacs side, the configuration is straightforward:
(setopt eglot-typescript-preset-lsp-server 'rass)
(setopt eglot-typescript-preset-rass-tools
'(typescript-language-server eslint oxlint))
(setopt eglot-typescript-preset-astro-lsp-server 'rass)
(setopt eglot-typescript-preset-astro-rass-tools
'(astro-ls eslint oxlint))
All three tools contribute diagnostics to the same Eglot session, and the preset
handles executable resolution from your project’s node_modules automatically.
Beyond plain TypeScript and JavaScript, eglot-typescript-preset supports:
astro-ls, with automatic TypeScript SDK detectionvscode-css-language-server, optionally combined with
tailwindcss-language-serversvelte-language-server, with TypeScript SDK path forwardingvue-language-server in hybrid mode, where Vue handles template
features and typescript-language-server with @vue/typescript-plugin
provides type checkingBoth packages support per-project overrides, so different projects can use different tools. There are two approaches.
.dir-locals.elDrop a .dir-locals.el in the project root. The preset packages declare their
variables as safe with appropriate values, so Emacs applies them without
prompting:
;;; .dir-locals.el for a Python project using basedpyright + ruff
((python-ts-mode
. ((eglot-python-preset-lsp-server . rass)
(eglot-python-preset-rass-tools . (basedpyright ruff)))))
;;; .dir-locals.el for a TypeScript project using biome instead of eslint
((typescript-ts-mode
. ((eglot-typescript-preset-lsp-server . rass)
(eglot-typescript-preset-rass-tools
. (typescript-language-server biome)))))
dir-locals-set-directory-classIf you’d rather not add Emacs-specific files to a project, configure it from your init file:
(dir-locals-set-class-variables
'my-python-project
'((python-ts-mode
. ((eglot-python-preset-lsp-server . rass)
(eglot-python-preset-rass-tools . (ty ruff))))))
(dir-locals-set-directory-class
(expand-file-name "~/devel/my-python-project") 'my-python-project)
(dir-locals-set-class-variables
'my-astro-project
'((typescript-ts-mode
. ((eglot-typescript-preset-lsp-server . rass)
(eglot-typescript-preset-rass-tools
. (typescript-language-server eslint oxlint))))
(astro-ts-mode
. ((eglot-typescript-preset-astro-lsp-server . rass)
(eglot-typescript-preset-astro-rass-tools
. (astro-ls eslint oxlint))))))
(dir-locals-set-directory-class
(expand-file-name "~/devel/my-astro-project") 'my-astro-project)
Of note:
eglot-typescript-preset package isn’t on MELPA at all yet. A
submission is pending.To install manually:
mkdir -p ~/devel
git clone https://github.com/mwolson/eglot-python-preset ~/devel/eglot-python-preset
git clone https://github.com/mwolson/eglot-typescript-preset ~/devel/eglot-typescript-preset
(add-to-list 'load-path (expand-file-name "~/devel/eglot-python-preset"))
(add-to-list 'load-path (expand-file-name "~/devel/eglot-typescript-preset"))
(require 'eglot-python-preset)
(setopt eglot-python-preset-lsp-server 'rass)
(setopt eglot-python-preset-rass-tools '(ty ruff))
(eglot-python-preset-setup)
(require 'eglot-typescript-preset)
(setopt eglot-typescript-preset-lsp-server 'rass)
(setopt eglot-typescript-preset-rass-tools '(typescript-language-server eslint))
(eglot-typescript-preset-setup)
Both packages require Emacs 30.2 or later and rass↗ v0.3.3 or later.
If you try either package, I’d appreciate hearing about your experience. The best way to reach me is through GitHub Discussions on the respective repos:
In particular, I’d love to hear from people using the JS/TS frameworks we support (Astro, Vue, Svelte, and Deno) as well as anyone working with CSS and Tailwind. If there’s a commonly-used framework or tool you’d like to see supported, please open a discussion. The more real-world usage these packages get, the more confident I’ll be promoting them from beta.
-1:-- Beta: Emacs Multi-LSP support for Python and Typescript frameworks (Post Mike Olson)--L0--C0--2026-03-16T00:00:00.000Z
Bozhidar Batsov has a nice post on delete-pair. It’s almost a case of the Baader-Meinhoff Phenomenon. Five months ago—before I wrote about it—I’d never heard of delete-pair and now it’s popping up on Batsov’s blog. Batsov, himself, doesn’t remember hearing about it before either.
For those who came in late, delete-pair does just what it says on the tin: delete the delimiter at point and its matching closing sibling. It’s actually quite flexible and usable in surprising ways as you can see from the_cecep’s original post that inspired mine.
The delete-pair command came to Batsov’s attention because it interacted poorly with his Neocaml package. That turned out to be because delete-pair makes assumptions that might not hold for languages with unusual syntax.
Batsov’s post is mostly a summary of what he found from researching the deletions of pairs in Emacs. For Lisp languages, all you need is paredit which has multiple ways of removing a delimiter pair. Probably the easiest is paredit-splice-sexp.
There’s also smartparens that also has several ways of removing a delimiter pair and has the advantage of working in all languages, not just Lisps. Batsov finds it too heavyweight for his needs but lots of folks swear by it.
Finally, there’s the builtin electric-pair-mode, which keeps delimiters balanced, pairs nicely with delete-pair, and is probably all most people need.
Take a look at Batsov’s post for more details and for a bit of Elisp that recreates Vim’s vim-surround. I was glad to see his post because I was right on the verge of forgetting delete-pair even though I’ve assigned it a keyboard shortcut.
-1:-- Removing Paired Delimiters In Emacs (Post Irreal)--L0--C0--2026-03-15T14:14:56.000Z
Continuing my Prelude modernization effort (see the previous post for context), another long-standing third-party dependency I was happy to drop was anzu.
When you’re searching with C-s in Emacs, you can see the current match highlighted, but you
have no idea how many total matches exist in the buffer or which one you’re currently on. Are
there 3 matches or 300? You just don’t know.
For years, I used the anzu package (an Emacs port of anzu.vim) to display match counts in the
mode-line. It worked well, but it was yet another third-party dependency to maintain – and one
that eventually ended up in the Emacs
orphanage, which is never a great sign for long-term
maintenance.
Emacs 27 introduced isearch-lazy-count:
(setopt isearch-lazy-count t)
With this enabled, your search prompt shows something like (3/47) – meaning you’re on the 3rd
match out of 47 total. Simple, built-in, and requires no external packages.
Unlike anzu, which showed counts in the mode-line, isearch-lazy-count displays them right in
the minibuffer alongside the search string, which is arguably a better location since your eyes
are already there while searching.
Two variables control how the count is displayed:
;; Prefix format (default: "%s/%s ")
;; Shows before the search string in the minibuffer
(setopt lazy-count-prefix-format "(%s/%s) ")
;; Suffix format (default: nil)
;; Shows after the search string
(setopt lazy-count-suffix-format nil)
If you prefer the count at the end of the prompt (closer to how anzu felt), you can swap them:
(setopt lazy-count-prefix-format nil)
(setopt lazy-count-suffix-format " [%s/%s]")
The count works with all isearch variants – regular search, regex search (C-M-s), and word
search. It also shows counts during query-replace (M-%) and query-replace-regexp
(C-M-%), which is very handy for knowing how many replacements you’re about to make.
The counting is “lazy” in the sense that it piggybacks on the lazy highlighting mechanism
(lazy-highlight-mode), so it doesn’t add significant overhead. In very large buffers, you
might notice a brief delay before the count appears, controlled by
lazy-highlight-initial-delay. One thing to keep in mind – if you’ve disabled lazy highlighting
for performance reasons, you’ll need to re-enable it, as the count depends on it.
If you haven’t read it already, check out my earlier article You Have No Idea How Powerful
Isearch Is for a deep dive
into what isearch can do. isearch-lazy-count pairs nicely with all the features covered there.
Between use-short-answers and isearch-lazy-count, that’s two third-party packages I was able
to drop from Prelude just by using built-in functionality. Keep hacking!
-1:-- isearch-lazy-count: Built-in Search Match Counting (Post Emacs Redux)--L0--C0--2026-03-15T05:40:00.000Z
I recently started a long overdue update of Emacs
Prelude, rebasing it on Emacs 29 as the minimum supported
version. This has been a great excuse to revisit a bunch of old configuration patterns and
replace them with their modern built-in equivalents. One of the first things I updated was the
classic yes-or-no-p hack.
By default, Emacs asks you to type out the full word yes or no for certain prompts –
things like killing a modified buffer or deleting a file. The idea is that this extra friction
prevents you from accidentally confirming something destructive, but in practice most people
find it annoying and want to just hit y or n.
For decades, the standard solution was one of these:
(fset 'yes-or-no-p 'y-or-n-p)
;; or equivalently:
(defalias 'yes-or-no-p 'y-or-n-p)
This worked by literally replacing the yes-or-no-p function with y-or-n-p at runtime. Hacky,
but effective – until native compilation came along in Emacs 28 and broke it. Native compilation
can hardcode calls to C primitives, which means fset/defalias sometimes has no effect on
yes-or-no-p calls that were already compiled. You’d set it up, and some prompts would still
ask for yes or no. Not fun.
Emacs 28 introduced the use-short-answers variable:
(setopt use-short-answers t)
That’s it. Clean, discoverable, native-compilation-safe, and officially supported. It makes
yes-or-no-p delegate to y-or-n-p internally, so it works correctly regardless of compilation
strategy.
If you’re maintaining a config that needs to support older Emacs versions as well, you can do:
(if (boundp 'use-short-answers)
(setopt use-short-answers t)
(fset 'yes-or-no-p 'y-or-n-p))
The Emacs maintainers intentionally designed yes-or-no-p to slow you down for destructive
operations. Enabling use-short-answers removes that friction entirely. In practice, I’ve never
accidentally confirmed something I shouldn’t have with a quick y, but it’s worth knowing the
tradeoff you’re making.
If you’re using GUI Emacs, you might also want to disable dialog boxes for a consistent experience:
(setopt use-dialog-box nil)
It’s also worth knowing that the related variable read-answer-short controls the same behavior
for multi-choice prompts (the ones using read-answer internally). Setting use-short-answers
affects both yes-or-no-p and read-answer.
This is one of those small quality-of-life improvements that Emacs has been accumulating in recent versions. Updating Prelude has been a nice reminder of how many rough edges have been smoothed over. Keep hacking!
-1:-- use-short-answers: The Modern Way to Tame yes-or-no Prompts (Post Emacs Redux)--L0--C0--2026-03-15T05:10:00.000Z
In Org Mode, when you use "Export to HTML - As HTML file and open", the resulting HTML file is loaded using a file:// URL. This means you can't load any media files. In my post about pronunciation practice, I wanted to test the playback without waiting for my 11ty-based static site generator to churn through the files.
simple-httpd lets you run a web server from Emacs. By default, the httpd-root is ~/public_html and httpd-port is 8085, but you can configure it to be somewhere else. Here I set it up to create a new temporary directory, and to delete that directory afterwards.
(use-package simple-httpd
:config
(setq httpd-root (make-temp-file "httpd" t))
:hook
(httpd-stop . my-simple-httpd-remove-temporary-root)
(kill-emacs . httpd-stop))
(defun my-simple-httpd-remove-temporary-root ()
"Remove `httpd-root' only if it's a temporary directory."
(when (file-in-directory-p httpd-root temporary-file-directory)
(delete-directory httpd-root t)))
The following code exports your Org buffer or subtree to a file in that directory, copies all the referenced local files (if they're newer) and updates the links in the HTML, and then serves it via simple-httpd. Note that it just overwrites everything without confirmation, so if you refer to files with the same name, only the last one will be kept.
(with-eval-after-load 'ox
(org-export-define-derived-backend 'my-html-served 'html
:menu-entry
'(?s "Export to HTML and Serve"
((?b "Buffer" my-org-serve--buffer)
(?s "Subtree" my-org-serve--subtree)))))
(defun my-org-serve--buffer (&optional async _subtreep visible-only body-only ext-plist)
(my-org-export-and-serve nil))
(defun my-org-serve--subtree (&optional async _subtreep visible-only body-only ext-plist)
(my-org-export-and-serve t))
;; Based on org-11ty--copy-files-and-replace-links
;; Might be a good idea to use something DOM-based instead
(defun my-html-copy-files-and-replace-links (info &optional destination-dir)
(let ((file-regexp "\\(?:src\\|href\\|poster\\)=\"\\(\\(file:\\)?.*?\\)\"")
(destination-dir (or destination-dir (file-name-directory (plist-get info :file-path))))
file-all-urls file-name beg
new-file file-re
unescaped)
(unless (file-directory-p destination-dir)
(make-directory destination-dir t))
(unless (file-directory-p destination-dir)
(error "%s is not a directory." destination-dir))
(save-excursion
(goto-char (point-min))
(while (re-search-forward file-regexp nil t)
(setq file-name (or (match-string 1) (match-string 2)))
(unless (or (string-match "^#" file-name)
(get-text-property 0 'changed file-name))
(setq file-name
(replace-regexp-in-string
"\\?.+" ""
(save-match-data (if (string-match "^file:" file-name)
(substring file-name 7)
file-name))))
(setq unescaped
(replace-regexp-in-string
"%23" "#"
file-name))
(setq new-file (concat
(if info (plist-get info :permalink) "")
(file-name-nondirectory unescaped)))
(unless (org-url-p file-name)
(let ((new-file-name (expand-file-name (file-name-nondirectory unescaped)
destination-dir)))
(condition-case err
(when (or (not (file-exists-p new-file-name))
(file-newer-than-file-p unescaped new-file-name))
(copy-file unescaped new-file-name t))
(error nil))
(when (file-exists-p new-file-name)
(save-excursion
(goto-char (point-min))
(setq file-re (concat "\\(?: src=\"\\| href=\"\\| poster=\"\\)\\(\\(?:file://\\)?" (regexp-quote file-name) "\\)"))
(while (re-search-forward file-re nil t)
(replace-match
(propertize
(save-match-data (replace-regexp-in-string "#" "%23" new-file))
'changed t)
t t nil 1)))))))))))
(defun my-org-export-and-serve (&optional subtreep)
"Export current org buffer (or subtree if SUBTREEP) to HTML and serve via simple-httpd."
(interactive "P")
(require 'simple-httpd)
(httpd-stop)
(unless httpd-root (error "Set `httpd-root'."))
(unless (file-directory-p httpd-root)
(make-directory httpd-root t))
(unless (file-directory-p httpd-root)
(error "%s is not a directory." httpd-root))
(let* ((out-file (expand-file-name (concat (file-name-base (buffer-file-name)) ".html")
httpd-root))
(html-file (org-export-to-file 'my-html-served out-file nil subtreep)))
;; Copy all the files and rewrite all the links
(with-temp-file out-file
(insert-file-contents out-file)
(my-html-copy-files-and-replace-links
`(:permalink "/") httpd-root))
(httpd-start)
(browse-url (format "http://localhost:%d/%s"
httpd-port
(file-name-nondirectory html-file)))))
Now I can use C-c C-e (org-export-dispatch), select the subtree with C-s, and use s s
to export a subtree to a webserver and have all the media files work. This took 0.46 seconds for my post on pronunciation practice and automatically opens the page in a browser window. In comparison, my 11ty static site generator took 5.18 seconds for a subset of my site (1630 files copied, 214 files generated), and I haven't yet hooked up monitoring it to Emacs, so I have to take an extra step to open the page in the browser when I think it's finished. I think exporting to HTML and serving it with simple-httpd will be much easier for simple cases like this, and then I can export to 11ty once I'm done with the basic checks.
You can e-mail me at sacha@sachachua.com.
-1:-- Org Mode: Export HTML, copy files, and serve the results via simple-httpd so that media files work (Post Sacha Chua)--L0--C0--2026-03-14T20:43:37.000Z
Just a quick post today to note that Protesilaos Stavrou (Prot) has published the video from his FOSS @ Oxford talk that I wrote about yesterday. Actually, he says that it’s a modified version of that talk so I don’t know if a video of the actual talk will appear or not.
In any event, the video tracked very well with the transcript that I wrote about in yesterday’s post. The video is a little easier to follow because you can see the changes to the text that he makes during his talk. If you’re the type of person that gets more out of a video than reading a transcript, be sure to take a look. The video is fairly long at 42 minutes, 40 seconds long so you’ll have to put some time aside for it but it’s definitely worth your while.
-1:-- Prot’s Video From His FOSS @ Oxford Talk (Post Irreal)--L0--C0--2026-03-14T14:13:49.000Z
I stopped using Magit with Emacs a few months ago. Initially this was just because Magit added a new dependency cond-let that was conflicting the upstream development of a macro by the same name, but I had already taken issue with the number of dependencies such as llama, and generally do not enjoy Transient-based user interfaces, so I ended up sticking with the descision.
First things first: The absence of Magit in my regular arsenal is noticeable and slightly annoying. There are things I have gotten so used to doing via Magit, that I had to look up how I could do them without. And it is about this experience that I want to write about here.
The first and most immediate difference is that when I have to perform some Git-action, I don’t call magit-status (which I had bound to C-c g instead of the default global binding C-x g). Instead I mostly use shell-command (M-!), or more specifically my “fork” shell-command+ that I had extended to always run certain commands asynchronously, such as git or even defer to the respective VC-mode commands. So M-! git diff RET actually calls vc-diff, instead of just spitting the output into “*Async Shell Command*” (even if in this specific case I’d usually invoke C-x v D (vc-root-diff)). Having the bash-completion package installed also helps to find the right flags for git subcommands.
In other operations I can now use VC-mode more effectively. As of Emacs 31, a number of useful features have been added (mostly by the now new Emacs maintainer Sean Whitton) such as the ability to edit previous commit messages by pressing e in the “*vc-changes-log*” buffer, which extends the previous capabilities to just amend an existing commit message more effectively. See etc/NEWS for more details.
But there are useful features in Magit that are actually composite commands. The ones I found missing the most were spinning of branches (creating a new branch and reverting the previous branch to the upstream position) and easy fixups of previous commits. When researching online, an idea I came up upon was to add git alii to my configuration. I have to admit that I was disinclined to do so, again not so much for necessarily technical reasons, but mostly because I associate the usage of a git alias with people who insist on saving time by writing git c instead of git commit (or worse yet, they add a shell alias gc). Instead, I recalled that any subcommand git foo checks if an executable git-foo is in PATH and can defer to that instead. So I wrote scripts for the most common use-cases that I ran into
git spinoff NEW-BRANCH-NAME, create a new branch NEW-BRANCH-NAME with the commits between HEAD and the upstream state of the current branch, and then reset the current branch to the upstream state.
git update, is basically just a shorthand for git pull --autostash --rebase.
git fixup COMMIT, tries to amend the changes in the staging area onto a COMMIT, and then rebase all subsequent commits onto the modified changes.
These are really basic scripts that I just invoke using M-!, no further magic involved.
Some Git commands require user input, which in a terminal would start a TUI editor like vi or GNU nano. As I am executing commands using M-!, this is an issue, since I am not emulating a proper terminal, and do not intend to do so. Magit handles this using the with-editor package, which I have been thinking about using as well, but for now I just set
(setenv "EDITOR" "ed")
in my init.el and have been running with it since. It is not ideal, but slightly funny, and 99% of the time all I have to do is write wq anyway.
All in all this was an interesting exercise, and for now it works well enough. I know I am more in the “integrating development environment” school of Emacs, so some of my practices might appears strange, but I hope some might also be intrigued. Finally, I have to clarify that this post is about how I work with Git, and is not a hit-piece on Magit or anyone working on Magit — the issues I take with Magit are due to my own preferences and requirements, and not normative statements on how everyone should use Git with Emacs.
-1:-- Emacs after Magit (Post Philip Kaludercic)--L0--C0--2026-03-14T11:49:48.000Z
The other day someone filed an issue against
my neocaml package, reporting surprising behavior with
delete-pair. My first reaction was – wait, delete-pair? I’ve been using Emacs for over 20
years and I wasn’t sure I had ever used this command. Time for some investigation!
delete-pair?delete-pair is a built-in Emacs command (defined in lisp/emacs-lisp/lisp.el) that deletes a
pair of matching characters – typically parentheses, brackets, braces, or quotes. You place
point on an opening delimiter, invoke delete-pair, and it removes both the opening and closing
delimiter.
Given that it lives in lisp.el, it was clearly designed with Lisp editing in mind
originally. And it was probably quite handy back in the day – before paredit came along and
made delete-pair largely redundant for Lisp hackers.
Here’s a simple example. Given the following code (with point on the opening parenthesis):
(print_endline "hello")
Running M-x delete-pair gives you:
print_endline "hello"
Simple and useful! Yet delete-pair has no default keybinding, which probably explains why
so few people know about it. If you want to use it regularly, you’ll need to bind it yourself:
(global-set-key (kbd "M-s-d") #'delete-pair)
Pick whatever keybinding works for you, of course. There’s no universally agreed upon binding for this one.
The issue that was reported boiled down to delete-pair not always finding the correct matching
delimiter. It uses forward-sexp under the hood to find the matching closer, which means its
accuracy depends entirely on the buffer’s syntax table and the major mode’s parsing
capabilities. For languages with complex or unusual syntax, this can sometimes lead to the wrong
delimiter being removed – not great when you’re trying to be surgical about your edits.
If you work with paired delimiters frequently, delete-pair is just one tool in a rich
ecosystem. Here’s a quick overview of the alternatives:
paredit is the gold standard for structured editing of Lisp code. I’ve
been a heavy paredit user for as long as I can remember – if you write any Lisp-family
language, it’s indispensable. paredit gives you paredit-splice-sexp (bound to M-s by
default), which removes the surrounding delimiters while keeping the contents intact. There’s
also paredit-raise-sexp (M-r), which replaces the enclosing sexp with the sexp at point –
another way to get rid of delimiters. And of course, paredit prevents you from creating
unbalanced expressions in the first place, which is a huge win.
Once you’ve got paredit in your muscle memory, you’ll never think about delete-pair again
(as I clearly haven’t).
Let’s see these commands in action. In the examples below, | marks the position of point.
paredit-splice-sexp (M-s) – removes the surrounding delimiters:
;; Before (point anywhere inside the inner parens):
(foo (bar| baz) quux)
;; After M-s:
(foo bar| baz quux)
paredit-raise-sexp (M-r) – replaces the enclosing sexp with the sexp at point:
;; Before:
(foo (bar| baz) quux)
;; After M-r:
(foo bar| quux)
Notice the difference: splice keeps all the siblings, raise keeps only the sexp at point and
discards everything else inside the enclosing delimiters.
smartparens is the most feature-rich option and works across all languages, not just Lisps. For unwrapping pairs, it offers a whole family of commands:
sp-unwrap-sexp – removes the enclosing pair delimiters, keeping the contentsp-backward-unwrap-sexp – same, but operating backwardsp-splice-sexp (M-D) – removes delimiters and integrates content into the parent expressionsp-splice-sexp-killing-backward / sp-splice-sexp-killing-forward – splice while killing content in one directionHere’s how the key ones look in practice:
sp-unwrap-sexp – removes the next pair’s delimiters:
# Before (point on the opening bracket):
result = calculate(|[x, y, z])
# After sp-unwrap-sexp:
result = calculate(|x, y, z)
sp-splice-sexp (M-D) – works like paredit’s splice, removes the innermost enclosing pair:
# Before (point anywhere inside the parens):
result = calculate(x + |y)
# After M-D:
result = calculate x + |y
sp-splice-sexp-killing-backward – splices, but also kills everything before point:
# Before:
result = [first, second, |third, fourth]
# After sp-splice-sexp-killing-backward:
result = |third, fourth
I used smartparens for a while for non-Lisp languages, but eventually found it a bit heavy for
my needs.
electric-pair-mode is the built-in option (since Emacs 24.1) that automatically inserts
matching delimiters when you type an opening one. It’s lightweight, requires zero configuration,
and works surprisingly well for most use cases. I’ve been using it as my go-to solution for
non-Lisp languages for a while now.
The one thing electric-pair-mode doesn’t offer is any way to unwrap/remove paired
delimiters. The closest it gets is deleting both delimiters when you backspace between an
adjacent empty pair (e.g., (|) – pressing backspace removes both parens). But that’s it –
there’s no unwrap command. That’s where delete-pair comes in handy as a complement.
Having played with Vim and its various
surround.vim-like plugins over the years, I have to
admit – I kind of miss that experience in Emacs, at least for removing paired
delimiters. surround.vim makes it dead simple: ds( deletes surrounding parens, ds" deletes
surrounding quotes. It works uniformly across all file types and feels very natural.
In Emacs, the story is more fragmented – paredit handles it beautifully for Lisps,
smartparens does it for everything but is a heavyweight dependency, and electric-pair-mode
just… doesn’t do it at all. delete-pair is the closest thing to a universal built-in
solution, but its lack of a default keybinding and its reliance on forward-sexp make it a bit
rough around the edges.
If you’re using electric-pair-mode and want a simple surround.vim-style “delete surrounding
pair” command without pulling in a big package, here’s a little hack that does the trick:
(defun delete-surrounding-pair (char)
"Delete the nearest surrounding pair of CHAR.
CHAR should be an opening delimiter like (, [, {, or \".
Works by searching backward for the opener and forward for the closer."
(interactive "cDelete surrounding pair: ")
(let* ((pairs '((?\( . ?\))
(?\[ . ?\])
(?{ . ?})
(?\" . ?\")
(?\' . ?\')
(?\` . ?\`)))
(closer (or (alist-get char pairs)
(error "Unknown pair character: %c" char))))
(save-excursion
(let ((orig (point)))
;; Find and delete the opener
(when (search-backward (char-to-string char) nil t)
(delete-char 1)
;; Find and delete the closer (adjust for removed char)
(goto-char (1- orig))
(when (search-forward (char-to-string closer) nil t)
(delete-char -1)))))))
(global-set-key (kbd "M-s-d") #'delete-surrounding-pair)
Now you can hit M-s-d ( to delete surrounding parens, M-s-d " for quotes, etc. It’s
deliberately naive – no syntax awareness, no nesting support – so it won’t play well with
delimiters inside strings or comments (it’ll happily match a paren in a comment if that’s what
it finds first). But for quick, straightforward edits it gets the job done.
TIP: If you’re looking for something closer to the surround.vim experience in Emacs (without going
full smartparens), check out surround.el. It’s a
lightweight package (available on MELPA) that provides surround.vim-style operations –
deleting, changing, and adding surrounding pairs – all through a single keymap. It supports both
“inner” and “outer” text selection modes and works uniformly across file types, which makes it a
nice middle ground between delete-pair and smartparens.
My current setup is:
paredit, no contest.electric-pair-mode for auto-pairing (I rarely need to unwrap something outside of Lisps)I think surround.el will pair well with electric-pair-mode, but I discovered it only recently
and I’ve yet to try it out in practice.
If you want a more powerful structural editing experience across all languages, smartparens is
hard to beat. It’s just more than I personally need outside of Lisp.
One of the greatest aspects of Emacs is that we get to learn (or relearn) something about it every other day. Even after decades of daily use, there are always more commands lurking in the corners, patiently waiting to be discovered.
Now, if you’ll excuse me, I’m going to immediately forget about delete-pair again. Keep hacking!
Update: A reader pointed me to this Reddit
thread
with more tips on using delete-pair (including using it to change surrounding
delimiters, surround.vim-style). Worth a read if you want to get more mileage
out of it.
-1:-- Removing Paired Delimiters in Emacs (Post Emacs Redux)--L0--C0--2026-03-14T08:30:00.000Z
Protesilaos Stavrou (Prot) recently gave a talk to the FOSS @ Oxford event on the power of Emacs and why he uses it. The video for the talk is not yet available but he does have the transcript that he used for delivering the presentation.
It was a nice talk that explained why Prot believes Emacs is a tool that everyone using a computer should be familiar with and make use of. The TL;DR is that Emacs is extensible and can integrate all the (text-based) tasks you need to perform on your computer. If, as a trivial example, you develop a color and font style that is pleasing to you, you need only implement it once: Emacs will use it for whatever tasks you like. Non trivially, you can arrange for various tasks to communicate with each other and share information if they need to.
Prot stresses a point that Irreal has often made: You don’t have to be a programmer to make good use of Emacs. Writers, teachers, liberal arts academics, and many others can and do leverage Emacs to make their work easier. Of course, if you know a bit of Elisp it’s even better because you can extend Emacs to do whatever you need. Prot himself is not a programmer but he taught himself Elisp and is now a significant contributor to Emacs.
The takeaway from Prot’s talk is that Emacs is all about freedom. Freedom to make whatever changes you like, freedom to explore the source code and see how Emacs does things, and freedom to share with others and learn from them.
Take a look at the transcript or wait for the video if you prefer visual presentations. In either case, it’s well worth your time.
-1:-- Prot On Emacs At FLOSS @ Oxford (Post Irreal)--L0--C0--2026-03-13T15:16:46.000Z
In the last article in the series, we talked about deleting files, which is scary and sad. In this article, I’m happy to change topics into creation territory as we learn how to create directories in the UNIXy way and the Emacs way.
Which way is better? I leave that up to you, but you should know both, because context matters, and there be times when you need one because the other one might not be available.
Remember, in your computer everything is a file, even directories are files that point to other files.
So let’s get into it.
Also, be sure to check out my DRM-free eBooks for your open source enjoyment (OSE):
I’ve never been a big fan of the mkdir (make directory) command in Linux. I like what it does, but it’s a bit too long. It’s not as sleek as the rm, mv, and cp commands. Nevertheless, it perfectly does what it needs to do—make directories.
mkdir new_directory mkdir -p path/to/nested/directory
In this instance, I do prefer the Emacs way. All you need to do is open up dired by your preferred method, and press the + sign. Easy. Now you have a new directory.
You also have an interactive M-x make-directory (or M-x mkdir) function if you prefer that.
Likewise, if you have to create new file with a directory that does exist, Emacs will prompt you to create that directory.

The post The Emacs Way: Create Directories appeared first on Chris Maiorana.
-1:-- The Emacs Way: Create Directories (Post Chris Maiorana)--L0--C0--2026-03-13T04:00:49.000Z
My French tutor gave me a list of sentences to help me practise pronunciation.
I can fuzzy-match these with the word timing JSON from WhisperX, like this.
Extract all approximately matching phrases(subed-record-extract-all-approximately-matching-phrases
sentences
"/home/sacha/sync/recordings/2026-02-20-raphael.json"
"/home/sacha/proj/french/analysis/virelangues/2026-02-20-raphael-script.vtt")
Then I can use subed-record to manually tweak them, add notes, and so on. I end up with VTT files like 2026-03-06-raphael-script.vtt. I can assemble the snippets for a session into a single audio file, like this:
I wanted to compare my attempts over time, so I wrote some code to use Org Mode and subed-record to build a table with little audio players that I can use both within Emacs and in the exported HTML.
This collects just the last attempts for each sentence during a number of my sessions (both with the tutor and on my own). The score is from the Microsoft Azure pronunciation assessment service. I'm not entirely sure about its validity yet, but I thought I'd add it for fun. * indicates where I've added some notes from my tutor, which should be available as a title attribute on hover. (Someday I'll figure out a mobile-friendly way to do that.)
(my-lang-summarize-segments
sentences
'(("/home/sacha/proj/french/analysis/virelangues/2026-02-20-raphael-script.vtt" . "Feb 20")
;("~/sync/recordings/processed/2026-02-20-raphael-tongue-twisters.vtt" . "Feb 20")
("~/sync/recordings/processed/2026-02-22-virelangues-single.vtt" . "Feb 22")
("~/proj/french/recordings/2026-02-26-virelangues-script.vtt" . "Feb 26")
("~/proj/french/recordings/2026-02-27-virelangues-script.vtt" . "Feb 27")
("~/proj/french/recordings/2026-03-03-virelangues.vtt" . "Mar 3")
("/home/sacha/sync/recordings/processed/2026-03-03-raphael-reference-script.vtt" . "Mar 3")
("~/proj/french/analysis/virelangues/2026-03-06-raphael-script.vtt" . "Mar 6")
("~/proj/french/analysis/virelangues/2026-03-12-virelangues-script.vtt" . "Mar 12"))
"clip"
#'my-lang-subed-record-get-last-attempt
#'my-lang-subed-record-cell-info
t
)
| Feb 20 | Feb 22 | Feb 26 | Feb 27 | Mar 3 | Mar 3 | Mar 6 | Mar 12 | Text |
| ▶️ 63* | ▶️ 96 | ▶️ 95 | ▶️ 94 | ▶️ 83 | ▶️ 83* | ▶️ 81* | ▶️ 88 | Maman peint un grand lapin blanc. |
| ▶️ 88* | ▶️ 95 | ▶️ 99 | ▶️ 99 | ▶️ 96 | ▶️ 89* | ▶️ 92* | ▶️ 83 | Un enfant intelligent mange lentement. |
| ▶️ 84* | ▶️ 97 | ▶️ 97 | ▶️ 96 | ▶️ 94 | ▶️ 95* | ▶️ 98* | ▶️ 99 | Le roi croit voir trois noix. |
| ▶️ 80* | ▶️ 85 | ▶️ 77 | ▶️ 94 | ▶️ 97 | ▶️ 92* | ▶️ 88 | Le témoin voit le chemin loin. | |
| ▶️ 72* | ▶️ 97 | ▶️ 95 | ▶️ 77 | ▶️ 92 | ▶️ 89* | ▶️ 86 | Moins de foin au loin ce matin. | |
| ▶️ 79* | ▶️ 95 | ▶️ 76 | ▶️ 95 | ▶️ 76 | ▶️ 90* | ▶️ 90* | ▶️ 79 | La laine beige sèche près du collège. |
| ▶️ 67* | ▶️ 99 | ▶️ 85 | ▶️ 81 | ▶️ 85 | ▶️ 99* | ▶️ 97* | ▶️ 97 | La croquette sèche dans l'assiette. |
| ▶️ 88* | ▶️ 99 | ▶️ 100 | ▶️ 100 | ▶️ 98 | ▶️ 100* | ▶️ 99* | ▶️ 100 | Elle mène son frère à l'hôtel. |
| ▶️ 77* | ▶️ 87 | ▶️ 99 | ▶️ 93 | ▶️ 87 | ▶️ 87* | ▶️ 99 | Le verre vert est très clair. | |
| ▶️ 100* | ▶️ 94 | ▶️ 100 | ▶️ 99 | ▶️ 99 | ▶️ 99* | ▶️ 100* | ▶️ 100 | Elle aimait manger et rêver. |
| ▶️ 78* | ▶️ 98 | ▶️ 99 | ▶️ 98 | ▶️ 98 | ▶️ 92* | ▶️ 88 | Le jeu bleu me plaît peu. | |
| ▶️ 78* | ▶️ 97 | ▶️ 85 | ▶️ 95 | ▶️ 85 | ▶️ 85 | Ce neveu veut un jeu. | ||
| ▶️ 73* | ▶️ 95 | ▶️ 95 | ▶️ 96 | ▶️ 97 | ▶️ 100 | Le feu bleu est dangereux. | ||
| ▶️ 87* | ▶️ 76 | ▶️ 65 | ▶️ 97 | ▶️ 85 | ▶️ 74* | ▶️ 85* | ▶️ 96 | Le beurre fond dans le cœur chaud. |
| ▶️ 84* | ▶️ 43 | ▶️ 85 | ▶️ 79 | ▶️ 75 | ▶️ 98 | Les fleurs de ma sœur sentent bon. | ||
| ▶️ 70* | ▶️ 86 | ▶️ 79 | ▶️ 76 | ▶️ 87 | ▶️ 84 | ▶️ 98 | Le hibou sait où il va. | |
| ▶️ 92* | ▶️ 95 | ▶️ 86 | ▶️ 92 | ▶️ 98 | ▶️ 99* | ▶️ 94 | L'homme fort mord la pomme. | |
| ▶️ 83* | ▶️ 73 | ▶️ 69 | ▶️ 81 | ▶️ 60 | ▶️ 96* | ▶️ 81 | Le sombre col tombe. | |
| ▶️ 39* | ▶️ 49 | ▶️ 69 | ▶️ 56 | ▶️ 69 | ▶️ 96* | ▶️ 94 | L'auto saute au trottoir chaud. | |
| ▶️ 82 | ▶️ 84 | ▶️ 85 | ▶️ 98 | ▶️ 94 | ▶️ 96* | ▶️ 99 | Le château d'en haut est beau. | |
| ▶️ 89 | ▶️ 85 | ▶️ 75 | ▶️ 91 | ▶️ 52 | ▶️ 75* | ▶️ 70* | ▶️ 98 | Le cœur seul pleure doucement. |
| ▶️ 98* | ▶️ 99 | ▶️ 99 | ▶️ 95 | ▶️ 93* | ▶️ 97* | ▶️ 99 | Tu es sûr du futur ? | |
| ▶️ 97 | ▶️ 93 | ▶️ 92 | ▶️ 85* | ▶️ 90 | Trois très grands trains traversent trois trop grandes rues. | |||
| ▶️ 94 | ▶️ 85 | ▶️ 97 | ▶️ 82* | ▶️ 92 | Je veux deux feux bleus, mais la reine préfère la laine beige. | |||
| ▶️ 91 | ▶️ 79 | ▶️ 87 | ▶️ 82* | ▶️ 94 | Vincent prend un bain en chantant lentement. | |||
| ▶️ 89 | ▶️ 91 | ▶️ 91 | ▶️ 84* | ▶️ 92 | La mule sûre court plus vite que le loup fou. | |||
| ▶️ 91 | ▶️ 93 | ▶️ 93 | ▶️ 92* | ▶️ 96 | Luc a bu du jus sous le pont où coule la boue. | |||
| ▶️ 88 | ▶️ 71 | ▶️ 94 | ▶️ 86* | ▶️ 92 | Le frère de Robert prépare un rare rôti rouge. | |||
| ▶️ 81 | ▶️ 84 | ▶️ 88 | ▶️ 67* | ▶️ 94 | La mule court autour du mur où hurle le loup. |
Pronunciation still feels a bit hit or miss. Sometimes I say a sentence and my tutor says "Oui," and then I say it again and he says "Non, non…" The /ʁ/ and /y/ sounds are hard.
I like seeing these compact links in an Org Mode table and being able to play them, thanks to my custom audio link type. It should be pretty easy to write a function that lets me use a keyboard shortcut to play the audio (maybe using the keys 1-9?) so that I can bounce between them for comparison.
If I screen-share from Google Chrome, I can share the tab with audio, so my tutor can listen to things at the same time. Could be fun to compare attempts so that I can try to hear the differences better. Hmm, actually, let's try adding keyboard shortcuts that let me use 1-8, n/p, and f/b to navigate and play audio. Mwahahaha! It works!
Update 2026-03-14: My tutor gave me a new set of tongue-twisters. When I'm working on my own, I find it helpful to loop over an audio reference with a bit of silence after it so that I can repeat what I've heard. I have several choices for reference audio:
Here I stumble through the tongue-twisters. I've included reference audio from Kokoro, gtts, and ElevenLabs for comparison.
(my-subed-record-analyze-file-with-azure
(subed-record-keep-last
(subed-record-filter-skips
(subed-parse-file
"/home/sacha/proj/french/analysis/virelangues/2026-03-13-raphael-script.vtt")))
"~/proj/french/analysis/virelangues-2026-03-13/2026-03-13-clip")
| Gt | Kk | Az | Me | ID | Comments | All | Acc | Flu | Comp | Conf | |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 1 | X: pont | 93 | 99 | 90 | 100 | 86 | Mon oncle peint un grand pont blanc. {pont} |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 2 | C'est mieux | 68 | 75 | 80 | 62 | 87 | Un singe malin prend un bon raisin rond. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 3 | Ouais, c'est ça | 83 | 94 | 78 | 91 | 89 | Dans le vent du matin, mon chien sent un bon parfum. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 4 | ok | 75 | 86 | 63 | 100 | 89 | Le soin du roi consiste à joindre chaque coin du royaume. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 5 | Ouais, c'est ça, parfait | 83 | 94 | 74 | 100 | 88 | Dans un coin du bois, le roi voit trois points noirs. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 6 | Ouais, parfait | 90 | 92 | 87 | 100 | 86 | Le feu de ce vieux four chauffe peu. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 7 | Ouais | 77 | 85 | 88 | 71 | 86 | Deux peureux veulent un peu de feu. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 8 | 77 | 78 | 75 | 83 | 85 | Deux vieux bœufs veulent du beurre. | |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 9 | Ouais, parfait | 92 | 94 | 89 | 100 | 89 | Elle aimait marcher près de la rivière. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 10 | Ok, c'est bien | 93 | 98 | 89 | 100 | 90 | Je vais essayer de réparer la fenêtre. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 11 | Okay | 83 | 87 | 76 | 100 | 89 | Le bébé préfère le lait frais. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 12 | 77 | 92 | 70 | 86 | 90 | Charlotte cherche ses chaussures dans la chambre. | |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 13 | Okay | 91 | 90 | 94 | 91 | 88 | Un chasseur sachant chasser sans son chien est-il un bon chasseur ? |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 14 | Ouais | 91 | 88 | 92 | 100 | 91 | Le journaliste voyage en janvier au Japon. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 15 | C'est bien (X: dans un) | 91 | 88 | 94 | 100 | 88 | Georges joue du jazz dans un grand bar. {dans un} |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 16 | C'est bien | 88 | 87 | 94 | 88 | 85 | Un jeune joueur joue dans le grand gymnase. |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 17 | 95 | 94 | 96 | 100 | 91 | Le compagnon du montagnard soigne un agneau. | |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 18 | 85 | 88 | 84 | 86 | 89 | La cigogne soigne l’agneau dans la campagne. | |
| 👂🏼 | 👂🏼 | 👂🏼 | ▶️ | 19 | grenouille | 71 | 80 | 68 | 75 | 86 | La grenouille fouille les feuilles dans la broussaille. |
(defun my-lang-subed-record-cell-info (item file-index file sub)
(let* ((sound-file (expand-file-name (format "%s-%s-%d.opus"
prefix
(my-transform-html-slugify item)
(1+ file-index))))
(score (car (split-string
(or
(subed-record-get-directive "#+SCORE" (elt sub 4)) "")
";")))
(note (replace-regexp-in-string
(concat "^" (regexp-quote (cdr file))
"\\(: \\)?")
""
(or (subed-record-get-directive "#+NOTE" (elt sub 4)) ""))))
(when (or always-create (not (file-exists-p sound-file)))
(subed-record-extract-audio-for-current-subtitle-to-file sound-file sub))
(org-link-make-string
(concat "audio:" sound-file "?icon=t"
(format "&source=%s&source-start=%s" (car file) (elt sub 1))
(format "&title=%s"
(url-hexify-string
(if (string= note "")
(cdr file)
(concat (cdr file) ": " note)))))
(concat
"▶️"
(if score (format " %s" score) "")
(if (string= note "") "" "*")))))
(defun my-lang-subed-record-get-last-attempt (item file)
"Return the last subtitle matching ITEM in FILE."
(car
(last
(seq-remove
(lambda (o) (string-match "#\\+SKIP" (or (elt o 4) "")))
(learn-lang-subed-record-collect-matching-subtitles
item
(list file)
nil
nil
'my-subed-simplify)))))
(defun my-lang-summarize-segments (items files prefix attempt-fn cell-fn &optional always-create)
(cons
(append
(seq-map 'cdr files)
(list "Text"))
(seq-map
(lambda (item)
(append
(seq-map-indexed
(lambda (file file-index)
(let* ((sub (funcall attempt-fn item file)))
(if sub
(funcall cell-fn item file-index file sub)
"")))
files)
(list item)))
items)))
(defun my-subed-record-analyze-file-with-azure (subtitles prefix &optional always-create)
(cons
'("Gt" "Kk" "Az" "Me" "ID" "Comments" "All" "Acc" "Flu" "Comp" "Conf")
(seq-map-indexed
(lambda (sub i)
(let ((sound-file (expand-file-name (format "%s-%02d.opus"
prefix
(1+ i))))
(tts-services
'(("gtts" . learn-lang-tts-gtts-say)
("kokoro" . learn-lang-tts-kokoro-fastapi-say)
("azure" . learn-lang-tts-azure-say)))
tts-files
(note (subed-record-get-directive "#+NOTE" (elt sub 4))))
(when (or always-create (not (file-exists-p sound-file)))
(subed-record-extract-audio-for-current-subtitle-to-file sound-file sub))
(setq
tts-files
(mapcar
(lambda (row)
(let ((reference (format "%s-%s-%02d.opus" prefix (car row) (1+ i) )))
(when (or always-create (not (file-exists-p reference)))
(funcall (cdr row)
(subed-record-simplify (elt sub 3))
'sync
reference))
(org-link-make-string
(concat "audio:" reference "?icon=t¬e=" (url-hexify-string (car row)))
"👂🏼")))
tts-services))
(append
tts-files
(list
(org-link-make-string
(concat "audio:" sound-file "?icon=t"
(format "&source-start=%s" (elt sub 1))
(if (and note (not (string= note "")))
(format "&title=%s"
(url-hexify-string note))
""))
"▶️")
(format "%d" (1+ i))
(or note ""))
(learn-lang-azure-subed-record-parse (elt sub 4))
(list
(elt sub 3)))))
subtitles)))
Some code for doing this stuff is in sachac/learn-lang on Codeberg.
You can e-mail me at sacha@sachachua.com.
-1:-- Comparing pronunciation recordings across time (Post Sacha Chua)--L0--C0--2026-03-13T01:11:45.000Z
Raw link: https://www.youtube.com/watch?v=M6ZHDJeG-dI
This is a holistic introduction to Emacs: how useful it is and how it champions free software. It is a modified version of the talk I did for the “FLOSS @ Oxford” event, organised by people at the University of Oxford. This is the page I wrote about that event: https://protesilaos.com/codelog/2026-03-12-my-emacs-talk-floss-oxford/.
Hello everyone! My name is Protesilaos, also known as “Prot”.
This presentation is a modified version of the talk I gave last night at the FLOSS @ Oxford event: https://ox.ogeer.org/event/computing-in-freedom-with-gnu-emacs-protesilaos-stavrou.
It was an event organised by people from the University of Oxford. I thank them for giving me the opportunity to participate in their programme.
I want to have this modified version here for people who do not read my website. They may not be aware that I talked at this Oxford event.
Having the video on this platform means that everyone can benefit from it.
In this presentation I will talk to you about GNU Emacs, or simply, “Emacs”. Emacs is a program you run on your computer. I am using it right now for this presentation.
Emacs is free or libre software. It allows you to read all of its source code, to modify it, and to share it with your customisations. Thus you contribute to—and benefit from—a community of welcoming Emacs users.
I will tell you what all this means in practice and how you can improve your computing experience by switching to Emacs.
When you first start using Emacs, it feels like a regular text editor program.
You move the cursor around and edit text. Nothing obviously impressive out-of-the-box.
As a text editor, Emacs is highly capable. It has all sorts of keyboard shortcuts that let you efficiently operate on text.
You can control Emacs without relying on the mouse, if you want.
Emacs supports the Unicode standard, which is essential for inclusivity of peoples.
The world’s scripts can be expressed in Emacs. I am a native Greek speaker.
I can use functionality that is built into Emacs to switch to the Greek alphabet in order to write something, such as to say «καλησπέρα», which means “good evening”.
I can even spell out “Dao De Jing” (道德经), which is the title of a book from ancient China.
Plus emoji: 🦚🦬🐉.
The multitude of scripts can be present in the same document.
This is an advantage for multilingual people like myself or those who do research that involves many natural languages.
Emacs can combine several fonts in the same page as well as different colours.
Each fonts can have its own attributes, such as for its relative size and typographic intensity.
Same idea for colours.
On my screen right now, I am already combining two different font styles: that of the heading and the body of the text.
Emacs does not limit you to a text-only interface. It can also display images and PDF documents. Below I have a link to an image file. I will now type a keyboard shortcut to reveal this image. And I will do it again to hide it.
This, by the way, is a spot somewhere in my mountains.
Although you can benefit from using Emacs as a generic text editor, what really appeals to people like me is the option to extend Emacs.
“Extend” here means to introduce new functionality; functionality that is not available in the default program you install on your computer.
These extensions are written in the same programming language as most of Emacs. It is a programming language called “Emacs Lisp” or “Elisp”.
You can extend Emacs on your own, by writing some program in Elisp, or you can download an existing extension that the community has made available.
For example, when I create a new extension for Emacs, I publish it under the terms of a free software license—the same terms that Emacs uses.
Others can then download my extension and use it as they prefer. If they want, they can make their own modifications on top, which may introduce other extensions that I had not thought of in my original implementation.
And if those users follow my example, then I can also benefit from their additions once they publish them.
As such, there exists a community of enthusiastic users of Emacs who care about sharing their works with the rest of the world.
Users can extend Emacs by running some Emacs Lisp program. Such a program can be as short as a single line. Or it can be as long as it needs to be. It does not matter.
Users run the program and Emacs immediately does what the program renders possible.
For example, I am doing this presentation inside of Emacs. But Emacs does not have a “presentation mode” built into it. I thus developed my own extension which empowers me to do what I am doing right now.
Let me toggle off my presentation mode to show you what I mean.
Notice that the display has changed.My main font is monospaced now.
The headings are smaller than they were before: they are the same size as the rest of the text. There is no number next to the heading anymore.
Then, there is a bar at the bottom of my screen, with information about what I am working on. On the side, there are line numbers, indicating where my cursor is in this file. Plus, my current line is highlighted with a distinct background colour. Let me shift it up and down to illustrate this point.
All those elements are useful while I am programming. But they look distracting when I wish to focus on some portion of text. So, I just type the keyboard shortcut I have and—voilà!—I get the style I prefer.
You may wonder: why do I even need a customisable text editor?
The answer is about control. You are in charge of what you use and how you use it. You can piece together a workflow that works the way you prefer.
This presentation mode I toggled on and off earlier behaves exactly how I want. I decided which set of interface tweaks to apply. Another user may have a different preference in this regard.
For instance, they may like having line numbers on the side of the screen. There is no right or wrong answer. What matters is that Emacs gives us the means to do what makes sense to us.
Now apply this principle to everything you can use Emacs for: this will generally be a text-centric project.
I run my agenda exclusively through Emacs.
I handle all my email correspondence with Emacs.
I do programming and I write prose, such as blog posts for my website and books or technical manuals.
For each of these, I know that Emacs will empower me to perform my tasks without arbitrary restrictions.
Emacs lets me use Elisp to modify how I do my emails, for instance, and how I present tasks in my custom agenda view.
Without Emacs, I would not be in a position to control my computing experience to the extent I do.
The reason is that I would be relying on many different applications. Each application has its own interface and design paradigms.
Each application is configured, if at all, in a way that is specific to it. Customisations in one application do not carry over to other applications.
And, if we consider the important implementation details, each application may configurable in its own programming language.
In other words, that is not an integrated computing experience.
To have the same degree of control that Emacs makes possible, I would have to hope that somehow all those disparate applications would conspire in my favour.
That is wishful thinking.
The reality is that piecing together many different applications is an exercise in frustration and the path to a life of ever-distracting context switching.
Having everything I need inside of Emacs ensures that things happen in a manner that is consistent.
All customisations are written in the same programming language, namely, Emacs Lisp.
What I define for one context, such as this “presentation mode”, can be used in another context.
For example, I can have this presentation style enabled when I read emails. Why?
Because it can make it more comfortable for me at a certain hour. And I can even automate this with conditional logic, so it happens on its own when I open a new email under certain circumstances.
When you work with many applications that do not play nicely together, you cannot do something that the developers have not envisaged.
For example, your email client likely does not have access to a “presentation mode”. Same for your other applications.
Similarly, your many applications will not necessarily know how to read and interpret the configurations you have in one application.
Suppose you define your favourite colour scheme for your email client. You take the time to consider the harmonies and use precise typography to your liking.
Now, you switch to your calendar application and none of that work carries over: you have to do it again, assuming it is even possible.
Colours and styles may seem like relatively small issues. But they are indicative of something greater: disparate applications do not work together seamlessly.
Emacs does not have this problem. You define something for one context you have in mind and, eventually, it can be used in another context that initially you had not even thought of.
For example, in my Emacs I wrote a small function to quickly copy the “thing” at where the cursor is. This is useful when I do programming, as the “thing” can be an entire expression, like the definition of a function. But the “thing” may also be a link that I got in my email.
I had not thought of that use-case in advance. Yet it was trivial to have my function do what I need in this once unforeseen situation.
The integrated computing environment of Emacs is more than the sum of its parts.
This is because you can combine different pieces of functionality in ways that the original developer had not foresaw.
You do not simply have your writing, your email, your agenda, et cetera, in Emacs.
You have the functionality of one in tandem with the functionality of another. And you draw linkages between them as you see fit.
Consider once again this presentation I am now doing. What I have in front of me is the transcript of my talk. This is a plain text document, which I can edit live. Let me CAPITALISE THIS to illustrate the point.
I have made this file look a little bit like a series of slides.
Notice that if I scroll up and down, which I will do shortly, you only get the current section I am reading from: you do not have access to the rest of the document. I will scroll up and down now.
This is a feature known as “narrowing”. Let me “widen” the view and then try to scroll again. You will now be exposed to the rest of the text.
The original developer of this “narrowing” facility did not know how someone like me would make use of it.
I have it here for my presentation. Each heading becomes its own pseudo-slide. I have narrowing for my emails, when I want to read a portion of the text in a more focused way. It is all about how I choose to do my computing.
For many years before switching to Emacs, I did not enjoy using the computer.
I needed too much time to accomplish every single task.
I could never find any of my files in a timely fashion because there was no program that would enforce on my behalf a predictable file-naming scheme.
All my notes were eventually not retrievable. This made them useless. Data you save is only good if you can find what you are searching for.
My music collection was inconsistent because I needed special software to write the metadata… In short, I was not as productive as I would like to be.
And, above all, it was not fun.
Most of my work at the time was centred around the email client and a word processor.
The email client had its own subsystem for handling reminders for tasks. The format of those tasks was not interoperable with other programs.
I could not access the tasks with my favourite text editor. I thus had to use the clunky interface of the email client, which was never designed for task management—and was not configurable.
And then I had all the cognitively burdensome annoyances of my two applications looking quite different from each other.
My emails did not behave like my documents, which made it harder for me to flip between the two and continue writing. I would roll my eyes each time.
Emacs has elevated my computing experience.
I have been much more productive ever since I switched to it. Allow me to demonstrate a tiny bit of what I do each day.
I will temporarily exit the presentation mode in this window.
Then, in the bottom half of my screen, I will open my email client to read a message I got.
Once you follow my switch to the email client, I will hide the window that shows this presentation.
After that I will switch to my agenda to record a task and review what I have to do.
All this is done inside of Emacs. Time for action!
What I just demonstrated is a very small part of what I do every single day.
There is much more, though I cannot cover it all in this presentation.
The point, however, is the consistency of the experience; consistency throughout.
I have customised my email client by writing some Emacs Lisp code for it. I have done the same for the custom agenda I have. And much more.
Every time I work with Emacs Lisp, I acquire skills that are applicable outside the confines of the problem I am solving.
For example, by configuring email the way I want, I pick up programming skills that I can then apply to the design of my custom agenda.
This is an investment that pays off more and more.
Emacs will adapt to match my evolving needs. Each new workflow I incorporate in my Emacs setup will thus benefit from all the knowledge and features I have accumulated.
I do not have to relearn everything because I am not switching to another application.
I do not have to throw away all the work I did all those years. It is here to stay.
I do not feel the pressure to try the new shiny app of the day. I did that many times and always regretted it. I lost my data and time in the process.
Because I am rooted in this stability of Emacs, I remain productive and efficient.
I mentioned earlier that Emacs is free or libre software. This means that you can read its source code, modify it, and share your changes with others.
Emacs has a license that gives users power. There is no corporation that can take Emacs away from us. It belongs to the community and we all tend to its wellness.
In the case of Emacs, software freedom is not just about the license. It informs how you use the program. Emacs makes such freedom an irriducible part of its functionality.
You can, at any moment, ask Emacs what does a keyboard shortcut actually do. What is the definition of a function. What is the value of a variable. And you may even access the source code to check for yourself.
I will demonstrate this right now.
I actually learnt to program in Emacs Lisp by exercising this freedom.
I would tinker with Emacs and continuously check on its state. What does this do? Which function is called by that keyboard shortcut? How is a program able to determine if the file is not saved?
I wanted to learn how, for example, we move down a line. From there, I learnt that we can move down many lines at once.
I then figured that we can move down the lines and then also do something else, such as place the cursor at the end of the line and create a pulse effect to bring attention to it.
Not only did I learn how to configure Emacs, I even wrote tens of extensions for it. I have also authored a libre book titled “Emacs Lisp Elements”. This freedom is not theoretical. I did not have a background in programming, yet was empowered to act and to grow as a person.
Emacs is extended with Emacs Lisp. If you know how to program in that language, you can be extra opinionated and particular about the way Emacs facilitates your work.
But even without any expertise of this sort, you can still do much of what you like. Remember that I started using Emacs without a background in programming.
Emacs blurs the distinction between user and developer. Many of the developers actually start out as users like myself. They learn along the way and, eventually, they contribute to the development of Emacs.
I even have written code that is in core Emacs: my modus-themes as
well as several other smaller patches.
The Emacs community has developed a rich corpus of extensions. You do not need to invent anything right away in order to be productive.
We call these extensions “packages”, as they are distributed in a way that makes them easy to install and then use directly.
The Emacs program you will download on your computer ships with plenty of packages built-in.
Depending on your needs, you may not even have to install anything from what the community has to offer.
Though if you want a package, it is fairly easy to get it and run it on your system.
Emacs is not picky about how you should use it. You are empowered to be opinionated.
For example, Emacs ships with a package called org or “Org mode”. At
its core, this is a markup language. I am using it right now in this
document.
Notice how lines that start with an asterisk function as headings. This is what the markup does.
Org lets you write documents, including books, handle your tasks, organise your agenda, and much more. It is a powerhouse.
There are so many things to discover in Emacs as well as the broader package ecosystem.
Emacs as a whole provides high quality documentation that explains everything.
When you install Emacs, you get with it plenty of technical manuals. There is also an interactive tutorial to help you make sense of the basics.
Furthermore, when you ask Emacs for help about the definition of a function or the value of a variable, you receive the documentation for the thing you are looking for.
The expectation for all contributions to the official Emacs program is that the code is well-documented and the manual is updated accordingly.
Core Emacs sets the standard of what good documentation looks like. Package developers follow this practice.
For example, my denote package has a manual that is over 7500 lines
long. It exceeds 52000 words. In it users find detailed instructions
as well as code snippets that they can copy and use outright. And this
is not the exception. All my packages are like that, to the extent
necessary. Most other developers do the same.
As a community, we have access to so much knowledge for free and in freedom. If we are committed enough, we can learn from others and thus become better ourselves. We do so in a spirit of sharing and caring. For me, specifically, all this was of great help. I am self-taught because I received all those great resources from the community. I consider it my duty to give back in kind.
Because Emacs is extensible, there is practically no limit to what you can do with it. At least this is the case for all tasks that are text-heavy.
Emacs will just gracefully evolve to match your requirements, provided you can extend it on your own or with a relevant package.
The downside, however, is that it is not easy to become proficient in it. If you are committed, you can learn the basics within the first few days.
Though you will need to invest a few weeks or months to become skillful. It depends on how much effort you put into it, what sort of work you are doing, and what your background is.
I learnt the basics within a few days. I started writing my own Emacs
Lisp within weeks. And within a year I had my modus-themes moved
into core Emacs.
Several “starter kits” are available to help you get started. They set things up so that you do not need to discover everything at the outset.
The new version of Emacs (Emacs 31) will even come with a “newcomers theme”, which configures several settings in advance.
These can make the learning curve a bit smoother. For me, anything that improves the onboarding experiences is a plus.
Though I do not think that Emacs will ever become “plug and play”. This is due to its sheer depth and extensibility. It does so much that you still need to invest the time and effort into learning it.
However you start, the most reliable study involves the manuals. Those are written for the benefit of the user. Read them carefully.
What I can say with confidence is that Emacs is not for tourists. You cannot switch to it with the expectation that you will have a good time right away.
No. That will not work. There simply is no shortcut to excellence.
I encourage you to take it one step at a time. Emacs will make you more productive, provided you are patient enough to unlock its virtually boundless potential.
Take it slow and be methodical. Rely on the official manual no matter your starting point. Read from it and gradually incorporate its insights into your workflow.
The community—myself included—has plenty of resources to complement that study. Blog posts, video tutorials, books… But do not skip the official manual. Learning it slowly means that you will become proficient faster than you otherwise would.
I already talked about the technical side of things with regard to the integrated computing environment. Now combine that with two facts:
Emacs is not old, it is timeless. This is because it can be extended in a spirit of freedom.
Whatever new technology or idea we have as a collective, we can eventually bring it into Emacs.
This way, our integrated computing environment adapts with the times.
Thus Emacs remains ever-relevant.
Couched in those terms, the initial effort you will put into learning Emacs is actually not that much.
You have to maintain a longer-term view of this project.
If you are patient, Emacs will be one of the most reliable tools you will ever use throughout your life. And I say this as a handy man myself, someone who uses many tools for manual labour, having built the house I am in, among others.
I switched to Emacs in the summer of 2019. It is almost 7 years already. I see no reason not to use it for the next 7 years, if I can.
I will still want to write articles, do programming, maintain my agenda, and probably make presentations like this one.
Remember that you will not learn Emacs over the weekend. You are in it for the long-term. Take it slow and you will enjoy the experience.
This is all I have for you today folks. Thank you very much for your attention!
You can find this and everything else I publish on my website: https://protesilaos.com.
-1:-- Computing in freedom with GNU Emacs (Post Protesilaos Stavrou)--L0--C0--2026-03-13T00:00:00.000Z
Speech synthesis has come a long way since I first tried out Emacspeak in 2002. Kokoro TTS and Piper offer more natural-sounding voices now, although the initial delay in loading the models and generating speech mean that they aren't quite ready to completely replace espeak, which is faster but more robotic. I've been using the Kokoro FastAPI through my own functions for working with various speech systems. I wanted to see if I could get Kokoro and other OpenAI-compatible text-to-speech services to work with either speechd-el or Emacspeak just in case I could take advantage of the rich functionality either provides for speech-synthesized Emacs use. speechd-el is easier to layer on top of an existing Emacs if you only want occasional speech, while emacspeak voice-enables many packages to an extent beyond speaking simply what's on the screen.
Speech synthesis is particularly helpful when I'm learning French because I can use it as a reference for what a paragraph or sentence should sound like. It's not perfect. Sometimes it uses liaisons that my tutor and Google Translate don't use. But it's a decent enough starting point. I also used it before to read out IRC mentions and compile notifications so that I could hear them even if I was paying attention to a different activity.
Here's a demonstration of speechd reading out the following lines using the code I've just uploaded to https://codeberg.org/sachac/speechd-ai:
There's about a 2-second delay between the command and the start of the audio for the sentence.
Note that speechd-speak-read-sentence fails in some cases where (forward-sentence 1) isn't the same place as (backward-sentence 1) (forward-sentence 1), which can happen when you're in an Org Mode list. I've submitted a patch upstream.
Aside from that, speechd-speak-set-language, speechd-speak-read-paragraph and
speechd-speak-read-region are also useful
commands. I think the latency makes this best-suited for reading paragraphs, or for shadowing sentences for language learning.
I'm still trying to figure out how to get speechd-speak to work as smoothly as I'd like. I think I've got it set up so that the server falls back to espeak for short texts so that it can handle words or characters better, and uses the specified server for longer ones. I'd like to get to the point where it can handle all the things that speechd usually does, like saying lines as I navigate through them or giving me feedback as I'm typing. Maybe it can use espeak for fast feedback character by character and word by word, and then use Kokoro TTS for the full sentence when I finish. Then it will be possible to use it to type things without looking at the screen.
After putting this together, I still find myself leaning towards my own functions because they make it easy to see the generated speech output to a file, which is handy for saving reference audio that I can play on my phone and for making replays almost instant. That could also be useful for pre-generating the next paragraph to make it flow more smoothly. Still, it was interesting making something that is compatible with existing protocols and libraries.
Posting it in case anyone else wants to use it as a starting point. The repository also contains the starting point for an Emacspeak-compatible speech server. See See speechd-ai/README.org for more details.
https://codeberg.org/sachac/speechd-ai
You can e-mail me at sacha@sachachua.com.
-1:-- Small steps towards using OpenAI-compatible text-to-speech services with speechd-el or emacspeak (Post Sacha Chua)--L0--C0--2026-03-12T15:00:06.000Z
Code formatters. Many people swear by them and always run their code through a formatter before checking it in. I’ve never seen the need for them. I have strong feelings about code format and I always write my code to that format: no need for a separate formatter. At the same time, I recognize that others have strong feelings about their preferred format so I never want to reformat someone else’s code.
There are, in every organization, control freaks who think it critically important that everyone’s code look the same. I’ve never accepted that premise and have ignored formatting guidelines. Somehow, I’ve always managed to get away with that minor recalcitrance.
Still, some people appear to like code formatters and for those people, Bozhidar Batsov has an interesting post on code formatters and how to use them in Emacs. It turns out that there’s a plethora of formatters with slightly different behavior to choose from
Happily the Lisp languages—which I use almost exclusively these days—don’t really need one since for a Lisp, code formatting is really just a matter of indentation, which Emacs handles on its own. Other languages need more work.
One of the problems with code formatters run from within Emacs is that they can freeze Emacs while they run. This is the case of formatters that run before the file is saved. There are formatters that run after the save by asynchronously reading the saved file, reformatting it, and writing it back to disk. This has the small defect that an unformatted file exists on disk for a short while.
Batsov’s post covers reformatters implementing both methods. Some of them allow extensive configuration of the reformatting rules. Others enforce a fixed format. Take a look at Batsov’s post for a really good rundown of all the possibilities.
-1:-- Code Formatting With Emacs (Post Irreal)--L0--C0--2026-03-12T14:25:03.000Z
There is now an in-buffer replace feature in ollama-buddy, so now an ollama response can work directly on your text, streaming the replacement in real-time, and giving you a simple accept/reject choice!, I have also added an smerge diff inline if desired to show the differences and give the user the ability to accept or reject
Here is how it works : https://www.youtube.com/watch?v=Po7Wqpk0sqY
The feature is tucked away in the transient menu. Here’s the workflow:
C-c O → Actions → W, or run M-x ollama-buddy-toggle-in-buffer-replace✎ indicator appears in your header line, confirming the mode is activeC-c o then pick your rewrite commandIt is also worth noting that each custom menu can be defined with a :destination option, for either in-buffer or chat, so the global in-buffer replace doesn’t need to be selected, and the custom transient menu can be tailored for each command. For example, in the default custom menu, refactor code and proofread have the destination of in-buffer set, but actions like git commit message or describe code will fall back to the global option, which by default is chat, which is probably what you would want for these options.
During streaming, you’ll see a dimmed, italic [Rewriting...] placeholder where your selection was. The new text then flows in with a highlight overlay.
Once the stream finishes, you’re dropped into a minor mode with two options:
C-c C-c → accept the changes (keep new text, clear highlighting)
C-c C-k → reject and restore your original text
The mode line helpfully shows [Rewrite?] while you’re deciding. It’s a bit like ediff but for AI-generated changes and uses smerge-mode.
What if the AI starts going off the rails halfway through? No problem. Press C-c C-k at any point during streaming and the network process stops immediately, restoring your original selection. No waiting for it to finish rambling!
Press C-c d and the original text gets inserted below the new text in the same buffer. Word-level differences are highlighted using smerge-refine-regions, so you can see exactly what changed at a glance.
Green overlays mark added or modified words. Red strikethrough overlays show what was removed. It’s proper granular diffing, not just a before-and-after comparison.
This workflow is particularly useful for:
It’s less about “write this for me from scratch” and more “help me iterate on what I’ve already got.”
For best results, I’ve found that smaller, focused selections work better than trying to rewrite entire files in one go. The AI has more context to work with, and you can make more granular decisions about what to keep.
Next up is probably some more polish on the diff highlighting, or perhaps exploring how this could work with multi-file projects. But for now, again I think this implementation is good enough.
-1:-- Ollama Buddy - In-Buffer LLM Streaming (Post James Dyer)--L0--C0--2026-03-12T11:09:00.000Z
Earlier today, at 20:00 Europe/Athens time, I provided an introduction to Emacs at the event FLOSS @ Oxford: https://ox.ogeer.org/event/computing-in-freedom-with-gnu-emacs-protesilaos-stavrou.
I had written the transcript ahead of time to make my presentation more accessible. The event was held live as a Jitsi call. There were questions from participants, which I answered. A recording of the event will be available before the end of this week. I will update this article to include a link to the video.
UPDATE 2026-03-16 07:49 +0200: The video is here: https://ogeer.org/ox/rec/emacs/.
Below is the text of my talk. It is titled “Computing in freedom with GNU Emacs”. Note that some parts of my presentation only make sense in the video format, though I tried to describe in the transcript what I was demonstrating.
Hello everyone! My name is Protesilaos, also known as “Prot”. I am joining you from the mountains of Cyprus. Cyprus is an island in the Eastern Mediterranean Sea.
In this presentation I will talk to you about GNU Emacs, or simply, “Emacs”. Emacs is a program you run on your computer. I am using it right now for this presentation.
Emacs is free or libre software. It allows you to read all of its source code, to modify it, and to share it with your customisations. Thus you contribute to—and benefit from—a community of welcoming Emacs users.
I will tell you what all this means in practice and how you can improve your computing experience by switching to Emacs.
When you first start using Emacs, it feels like a regular text editor program. You move the cursor around and edit text. Nothing obviously impressive out-of-the-box. As a text editor, Emacs is highly capable. It has all sorts of keyboard shortcuts that let you efficiently operate on text. You can control Emacs without relying on the mouse, if you want.
Emacs supports the Unicode standard, which is essential for inclusivity of peoples. The world’s scripts can be expressed in Emacs. I am a native Greek speaker. I can use functionality that is built into Emacs to switch to the Greek alphabet in order to write something, such as to say «καλησπέρα», which means “good evening”. I can even spell out “Dao De Jing” (道德经), which is the title of a book from ancient China. Plus emoji: 🙃.
The multitude of scripts can be present in the same document. This is an advantage for multilingual people or those who do research that involves many natural languages.
Emacs can combine several fonts in the same page as well as different colours. Each fonts can have its own attributes, such as for its relative size and typographic intensity. Same idea for colours. On my screen right now, I am already combining two different font styles: that of the heading and the body of the text.
Emacs does not limit you to a text-only interface. It can also display images and PDF documents. Below I have a link to an image file. I will now type a keyboard shortcut to reveal this image. And I will do it again to hide it.
[Here is an image that I do not need to reproduce on my website: the specific image does not matter]
This, by the way, is a spot somewhere in my mountains.
Although you can benefit from using Emacs as a generic text editor, what really appeals to people like me is the option to extend Emacs. “Extend” here means to introduce new functionality; functionality that is not available in the default program you install on your computer.
These extensions are written in the same programming language as most of Emacs. It is a programming language called “Emacs Lisp” or “Elisp”. You can extend Emacs on your own, by writing some program in Elisp, or you can download an existing extension that the community has made available.
For example, when I create a new extension for Emacs, I publish it under the terms of a free software license—the same terms that Emacs uses. Others can then download my extension and use it as they prefer. If they want, they can make their own modifications on top, which may introduce other extensions that I had not thought of in my original implementation. And if those users follow my example, then I can also benefit from their additions once they publish them.
As such, there exists a community of enthusiastic users of Emacs who care about sharing their works with the rest of the world.
Users can extend Emacs by running some Emacs Lisp program. Such a program can be as small as a single line. Or it can be as long as it needs to be. It does not matter. Users run the program and Emacs immediately does what the program renders possible.
For example, I am doing this presentation inside of Emacs. But Emacs does not have a “presentation mode” built into it. I thus developed my own extension which empowers me to do what I am doing right now. Let me toggle off my presentation mode to show you what I mean.
Notice that the display has changed. My main font is monospaced now. The headings are smaller than they were before: they are the same size as the rest of the text. There is no number next to the heading anymore. Then, there is a bar at the bottom of my screen, with information about what I am working on. On the side, there are line numbers, indicating where my cursor is in this file. Plus, my current line is highlighted with a distinct background colour. Let me shift it up and down to illustrate this point.
All those elements are useful while I am programming. But they look distracting when I wish to focus on some portion of text. So, I just type the keyboard shortcut I have and—voilà!—I get the style I prefer.
You may wonder: why do I even need a customisable text editor? The answer is about control. You are in charge of what you use and how you use it. You can piece together a workflow that works the way you prefer.
This presentation mode I toggled on and off earlier behaves exactly how I want. I decided which set of interface tweaks to apply. Another user may have a different preference in this regard. For instance, they may like having line numbers on the side of the screen. There is no right or wrong answer. What matters is that Emacs gives us the means to do what makes sense to us.
Now apply this principle to everything you can use Emacs for: this will generally be a text-centric project. I run my agenda exclusively through Emacs. I handle all my email correspondence with Emacs. I do programming and I write prose, such as blog posts for my website and books or technical manuals.
For each of these, I know that Emacs will empower me to perform my tasks without arbitrary restrictions. Emacs lets me use Elisp to modify how I do my emails, for instance, and how I present tasks in my custom agenda view.
Without Emacs, I would not be in a position to control my computing experience to the extent I do. The reason is that I would be relying on many different applications. Each application has its own interface and design paradigms. Each application is configured, if at all, in a way that is specific to it. Customisations in one application do not carry over to other applications. And, if we consider the important implementation details, each application may configurable in its own programming language.
In other words, that is not an integrated computing experience. To have the same degree of control that Emacs makes possible, I would have to hope that somehow all those disparate applications would conspire in my favour. That is wishful thinking. The reality is that piecing together many different applications is an exercise in frustration and the path to a life of ever-distracting context switching.
Having everything I need inside of Emacs ensures that things happen in a manner that is consistent. All customisations are written in the same programming language, namely, Emacs Lisp. What I define for one context, such as this “presentation mode”, can be used in another context. For example, I can have this presentation style enabled when I read emails. Why? Because it can make it more comfortable for me at a certain hour. And I can even automate this, so it happens on its own when I open a new email.
When you work with many applications that do not play nicely together, you cannot do something that the developers have not envisaged. For example, your email client likely does not have access to a “presentation mode”. Same for your other applications.
Similarly, your many applications will not necessarily know how to read and interpret the configurations you have in one application. Suppose you define your favourite colour scheme for your email client. You take the time to consider the harmonies and use precise typography to your liking. Now, you switch to your calendar application and none of that work carries over: you have to do it again, assuming it is even possible.
Colours and styles may seem like relatively small issues. But they are indicative of something greater: disparate applications do not work together seamlessly.
Emacs does not have this problem. You define something for one context you have in mind and, eventually, it can be used in another context that initially you had not even thought of. For example, in my Emacs I wrote a small function to quickly copy the “thing” at where the cursor is. This is useful when I do programming, as the “thing” can be an entire expression, like the definition of a function. But the “thing” may also be a link that I got in my email. I had not thought of that use-case in advance.
The integrated computing environment of Emacs is more than the sum of its parts. This is because you can combine different pieces of functionality in ways that the original developer had not foresaw. You do not simply have your writing, your email, your agenda, et cetera, in Emacs. You have the functionality of one in tandem with the functionality of another. And you draw linkages between them as you see fit.
Consider once again this presentation I am now doing. What I have in front of me is the transcript of my talk. This is a plain text document, which I can edit live. Let me CAPITALISE THIS to illustrate the point. But I have made this file look a little bit like a series of slides. Notice that if I scroll up and down, which I will do now, you only get the current section I am reading from: you do not have access to the rest of the document. This is a feature known as “narrowing”. Let me “widen” the view and then try to scroll again. You will now be exposed to the rest of the text.
The original developer of this “narrowing” facility did not know how someone like me would make use of it. I have it here for my presentation. Each heading becomes its own pseudo-slide. I have narrowing for my emails, when I want to read a portion of the text in a more focused way. It is all about how I choose to do my computing.
For many years before switching to Emacs, I did not enjoy using the computer. I needed too much time to accomplish every single task. I could never find any of my files in a timely fashion because there was no program that would enforce on my behalf a predictable file-naming scheme.
All my notes were eventually not retrievable. My music collection was inconsistent because I needed special software to write the metadata… In short, I was not as productive as I would like to be. And, above all, it was not fun.
Most of my work at the time was centred around the email client and a word processor. The email client had its own subsystem for handling reminders for tasks. The format of those tasks was not interoperable with other programs. I could not access it with my favourite text editor. I thus had to use the clunky interface of the email client, which was never designed for task management—and was not configurable.
And then I had all the cognitively burdensome annoyances of my two applications looking quite different from each other. My emails did not behave like my documents, which made it harder for me to flip between the two and continue writing.
Emacs has elevated my computing experience. I have been much more productive ever since I switched to it. Allow me to demonstrate a tiny bit of what I do each day. I will temporarily exit the presentation mode in this window. Then, in the bottom half of my screen, I will open my email client to read a message I got. After that I will switch to my agenda to record a task and review what I have to do. All this is done inside of Emacs. Time for action!
What I just demonstrated is a very small part of what I do every single day. There is much more, though I cannot cover it all in this presentation. The point, however, is the consistency of the experience; consistency throughout.
I have customised my email client by writing some Emacs Lisp code for it. I have done the same for the custom agenda I have. And much more.
Every time I work with Emacs Lisp, I acquire skills that are applicable outside the confines of the problem I am solving. For example, by configuring email the way I want, I pick up programming skills that I can then apply to the design of my custom agenda.
This is an investment that pays off more and more. Emacs will grow or shrink to match my evolving needs. Each new workflow I incorporate in my Emacs setup will thus benefit from all the knowledge and features I have accumulated.
I do not have to relearn everything. I do not have to throw away all the work I did. It is here to stay. I do not feel the pressure to try the new shiny app of the day. And, because I am rooted in this stability, I remain productive and efficient.
I mentioned earlier that Emacs is free or libre software. This means that you can read its source code, modify it, and share your changes with others. Emacs has a license that gives users power. There is no corporation that can take Emacs away from us. It belongs to the community and we all tend to its wellness.
Software freedom is not just about the license. Emacs makes such freedom an irriducible part of its functionality. You can, at any moment, ask Emacs what does a keyboard shortcut actually do. What is the definition of a function. What is the value of a variable. And you may even access the source code to check for yourself.
I actually learnt to program in Emacs Lisp by exercising this freedom. I would tinker with Emacs and continuously check on its state. I wanted to learn how, for example, we move down a line. From there, I learnt that we can move down many lines at once. I then figured that we can move down the lines and then also do something else, such as place the cursor at the end of the line and create a pulse effect to bring attention to it.
Not only did I learn how to configure Emacs, I even wrote tens of extensions for it. I have also authored a libre book titled “Emacs Lisp Elements”. This freedom is not theoretical. I did not have a background in programming, yet was empowered to act.
Emacs is extended with Emacs Lisp. If you know how to program in that language, you can be extra opinionated and particular about the way Emacs facilitates your work.
But even without any expertise of this sort, you can still do much of what you like. This is because the Emacs community has developed a rich corpus of extensions. We call these extensions “packages”, as they are distributed in a way that makes them easy to install and then use directly.
The Emacs program you will download on your computer ships with plenty of packages built-in. Depending on your needs, you may not even have to install anything from what the community has to offer.
For example, Emacs ships with a package called org or “Org mode”. At
its core, this is a markup language. I am using it right now in this
document. Notice how lines that start with an asterisk function as
headings. This is what the markup does. Org lets you write documents,
including books, handle your tasks, organise your agenda, and much
more. It is a powerhouse. There are so many things to discover. Emacs
provides high quality documentation that explains everything.
When you install Emacs, you get with it plenty of technical manuals. There is also an interactive tutorial to help you make sense of the basics. Furthermore, when you ask Emacs for help about the definition of a function or the value of a variable, you receive the documentation for the thing you are looking for.
The expectation for all contributions to the official Emacs program is that the code is well-documented and the manual is updated accordingly.
This is true also for packages that the community develops. For
example, my denote package has a manual that is over 7500 lines
long. It exceeds 52000 words. In it users find detailed instructions
as well as code snippets that they can copy and use outright. And this
is not the exception. All my packages are like that, to the extent
necessary. Most other developers do the same.
As a community, we have access to so much knowledge for free and in freedom. If we are committed enough, we can learn from others and thus become better ourselves. We do so in a spirit of sharing and caring. For me, specifically, all this was of great help. I am self-taught because I received all those great resources from the community. I consider it my duty to give back in kind.
Because Emacs is extensible, there is practically no limit to what you can do with it. At least this is the case for all tasks that are text-heavy. Emacs will just gracefully evolve to match your requirements, provided you know how to extend it.
The downside, however, is that it is not easy to become proficient in it. If you are committed, you can learn the basics within the first few days. Though you will need to invest a few weeks or months to become skillful. It depends on how much effort you put into it.
What I can say with confidence is that Emacs is not for tourists. You cannot switch to it with the expectation that you will have a good time right away. No. That will not work. There simply is no shortcut to excellence.
I thus encourage you to adjust your expectations. Emacs will make you more productive, provided you are patient enough to unlock its virtually boundless potential. Take it slow and be methodical. Rely on the official manual. Read from it and gradually incorporate its insights into your workflow. The community has plenty of resources to complement that study. But do not skip the official manual. Learning it slowly means that you will become proficient faster than you otherwise would.
I already talked about the technical side of things with regard to the integrated computing environment. Now combine that with two facts:
Emacs is not old, it is timeless. This is because it can be extended in a spirit of freedom. Whatever new technology or idea we have as a collective, we can eventually bring it into Emacs. This way, our integrated computing environment adapts with the times.
Couched in those terms, the initial effort you will put into learning Emacs is actually not that much. You have to maintain a longer-term view of this project. If are patient, Emacs will be one of the most reliable tools you will ever use throughout your life. And I say this as a handy man myself, having built the house I am in, among others.
I switched to Emacs in the summer of 2019. It is almost 7 years already. I see no reason not to use it for the next 7 years, if I can. I will still want to write articles, do programming, maintain my agenda, and probably make presentations like this one.
Remember that you will not learn Emacs over the weekend. You are in it for the long-term. Take it slow and you will enjoy the experience.
This is all I have for you today folks. Thank you very much for your attention!
-1:-- My Emacs talk for FLOSS @ Oxford (Post Protesilaos Stavrou)--L0--C0--2026-03-12T00:00:00.000Z
We got quite a few agent-shell additions since the last post, so let's go through the highlights as of v0.47.1.
agent-shell?Agent shell is a native Emacs mode to interact with LLM agents powered by ACP (Agent Client Protocol).
agent-shell has been attracting quite a few users. Many of you are working in tech where employers are happily paying for IDE subscriptions and LLM tokens to improve productivity. If you are using agent-shell for work, consider getting your employer to give back by sponsoring the project.
I also know many of you work at AI companies offering paid agents like Claude Code, Copilot, Gemini, Codex, etc. all supported by agent-shell. Please nudge your employers to help fund projects like agent-shell, which are making their services available to more users.
Let's get this one out of the way, as it needs actioning. Both the npm package and the CLI agent have been renamed from claude-code-acp to claude-agent-acp (to align with Anthropic's branding guidelines). If you're using Claude Code, you'll need to update:
npm remove -g @zed-industries/claude-code-acp
npm install -g @zed-industries/claude-agent-acp
If you had customized agent-shell-anthropic-claude-acp-command, update it to point to claude-agent-acp.

This was a biggie. How sessions are loaded is now configurable via agent-shell-session-strategy. When set to 'new, starting a new shell delivers a fully bootstrapped session before presenting you with the shell prompt. This means the ACP handshake, authentication, and session creation all happen upfront.
You can enable this flow with:
(setq agent-shell-session-strategy 'new)
What's the benefit? Bootstrapped sessions enable changing models and session modes (Planning, Don't ask, Skip permissions, etc…) before submitting your first prompt.

For the time being, the existing (deferred) behaviour is still offered via 'new-deferred. Just set as follows:
(setq agent-shell-session-strategy 'new-deferred)
Probably the most requested feature and also facilitated by the bootstrapping changes. agent-shell-session-strategy also unlocks session resume. Set it to 'prompt and every time either M-x agent-shell-new-shell or C-u M-x agent-shell are invoked, you'll be offered to resume previous sessions or start a new one.
(setq agent-shell-session-strategy 'prompt)

Alternatively, you can set to 'latest to always resume the most recent session in current project.
Under the hood, there are two ways to pick up from previous session: session/resume (lightweight, no message replay) and session/load (full history replay). By default, agent-shell prefers resuming (controlled by agent-shell-prefer-session-resume). Please favor resuming for the time being as loading has more edge cases to sort out still.
Note: Both resuming and loading sessions are agent-dependent. Some agents may not yet support either, especially as the features aren't yet considered stable in Agent Client Protocol (see session/list spec).
This feature was a collaboration between @farra, @travisjeffery, and myself.
You can now use agent-shell-send-clipboard-image (#285 by @dangom) to send images straight from your clipboard into agent-shell. Clipboard images are saved to .agent-shell/screenshots in your project root and inserted into the shell buffer as context.
Note: You'll need either pngpaste or xclip installed on your system for the feature to automatically kick in.
In addition, we now have agent-shell-yank-dwim: if the clipboard has an image, it pastes it as context. Otherwise, it yanks text as usual. In other words, copy an image anywhere to your system's clipboard and paste/yank into the buffer as usual (typically via C-y).

Status labels and tool call titles rendering got some improvements. Status reporting is generally more compact, redundant text is dropped from tool call titles, and tool status/kind shortening has been consolidated.

agent-shell now renders images inline. When agents output images (charts, diagrams, screenshots, etc.), they display directly in the shell buffer. You may need to nudge the agent to output image paths in the expected format so agent-shell can pick up.

Markdown images:

Any of the following in a line of their own are supported also:
/path/to/image.png
file:///path/to/image.png
./output/chart.png
~/screenshots/demo.png
Recognized image formats depend on what your Emacs was built with (typically png, jpeg, gif, svg, webp, tiff, etc. via image-file-name-extensions).
While on the topic of image rendering, this works particularly well when coupled with charting agent skills. I shared some of these over at emacs-skills, demoed in episode 13 of the Bending Emacs series.
Tables are now rendered using overlays (#17 by @ewilderj).

Tracking usage now possible (#270 by @Lenbok):
agent-shell-show-context-usage-indicator.M-x agent-shell-show-usage to check token counts, context window usage, and cost in the minibuffer.r- An optional end-of-turn usage summary can be enabled via (setq agent-shell-show-usage-at-turn-end t).If keen to run multiple agents on the same repo without stepping on each other's work, M-x agent-shell-new-worktree-shell facilitates this via git worktrees (#255 by @nhojb).
You can now send context to a specific shell using agent-shell-send-file-to, agent-shell-send-region-to, agent-shell-send-clipboard-image-to. and agent-shell-send-screenshot-to. These prompt you to pick a target shell.
Both M-x agent-shell and agent-shell-send-dwim are now prefix-aware. C-u forces a new shell, while C-u C-u prompts you to pick a target shell.
Compose buffers now support file (via @) and command (via /) completions. It is now also possible to browse previous pages via C-c C-p and come back to your prompt draft.
There's also prompt history navigation/insertion when composing prompts via M-p (previous), M-n (next), and M-r (search).
Bringing context into viewport compose buffers is now more robust. For example, carrying context into a new viewport compose buffer is now supported (#383 by @liaowang11).
While viewport interaction was introduced in the previous post, it is now my preferred way of interacting with agent-shell. You can enable via (setq agent-shell-prefer-viewport-interaction t). In any case, viewport buffers got a handful of quality-of-life improvements.
Single key replies without needing to open a compose/reply buffer:
y sends "yes" (handy for quickly answering agent questions).1-9 sends digits (handy for quickly choosing options).m sends "more" (handy for requesting more of the same kind of data).a sends "again" (handy for requesting to carry out instructions again).agent-shell-context-sources lets you configure which DWIM context sources are considered by M-x agent-shell, agent-shell-send-dwim, and compose buffers. You can control the order sources are checked and add custom functions. Defaults to files, region, error, and line.
I'm always on the lookout for some DWIM goodness. If you add your own context function, I'd love to hear about it.
While on the topic of context sources, in addition to picking up flymake errors at point, flycheck errors are now automatically recognized (#219 by @Lenbok).
Diff buffers got some love too, now with syntax highlighting (#198 by @Azkae). There's also a new agent-shell-diff-mode-map for customizing diff keybindings, which avoid inheriting unsupported features from the parent mode. You can also press f to open the modified file from a diff buffer.
Additionally, you can now press C-c C-c from an agent-shell-diff buffer to reject all changes (same binding as the shell itself).
You can now programmatically subscribe to agent-shell events like initialization steps, tool call updates, file writes, permission responses, permission requests, and turn completions. This opens the door for building integrations on top of agent-shell.
(agent-shell-subscribe-to
:shell-buffer (current-buffer)
:event 'file-write
:on-event (lambda (event)
(message "File written: %s"
(alist-get :path (alist-get :data event)))))
Permission dialogs got a few improvements. Amongst them, automatic navigation to the next pending dialog and also making executed commands more prominent for some agents.
You can now programmatically respond to permission requests via agent-shell-permission-responder-function. A built-in agent-shell-permission-allow-always handler is provided to auto-approve everything (use with caution):
(setq agent-shell-permission-responder-function #'agent-shell-permission-allow-always)
OAuth token now supported for Claude Code (#339 by @chemtov).
Markdown transcripts generation needed love. Thank you @Idorobots and @systemfreund for the improvements (#374, #325, and #326).
With (setq agent-shell-show-session-id t), session IDs are now displayed in header as well as session lists (#363 by @Cy6erBr4in).
agent-shell-cwd-function now enables customizing how agent-shell determines the working directory sent to agents.
agent-shell-dot-subdir-function now lets you customize where agent-shell keeps per-project files (#378 by @zackattackz).
agent-shell-mcp-servers now accept lambda functions too (#237 by @matthewbauer). Useful for setups like claude-code-ide using dynamic MCP details.
agent-shell-buffer-name-format now makes buffer naming configurable. Choose between the default title case ("Claude Code Agent @ My Project"), kebab-case ("claude-code-agent @ my-project"), or provide your own function (#256 by @nhojb).
agent-shell-write-inhibit-minor-modes lets you temporarily disable modes (like auto-formatting entire file) when agents write files (#224 by @ultronozm).
File autocompletion is now more performant for larger repositories (#262 by @perfectayush).
When running shells inside a container, a [C] indicator now shows up in the modeline (#250 by @ElleNajt).
Graphical header rendering is more robust now (#275 by @nhojb).
The busy/working indicator now offers multiple visual styles, customizable via agent-shell-busy-indicator-frames (#280 by @Lenbok).
agent-shell sessions from your mobile or any other device via Slack (by @ElleNajt).agent-shell with inter-agent communication, task tracking, and project-level dispatching (by @ElleNajt).agent-shell sessions (by @gveres).Thank you to all contributors for these improvements!
when-let (@jinnovation)@mentioned paths in devcontainers (@fritzgrabo)when-let binding issue (@byronclark)user_message_chunk session update type (@Lenbok)image require (@timvisher):tool-calls state maintenance (@vermiculus)C-c C-c to send (@HIRANO-Satoshi)agent-shell-send-dwim to transient (@matthewbauer)replace-buffer-contents timeout to prevent freeze on large diffsfind-file-noselect during agent writesuser_message_chunk session updates no longer dump raw JSONsession/request_permission with argv array commandsagent-shell-send-region no longer fails silently without a regionsession/request_permission (fix by @Azkae)loadSession regressionagent-shell-select-config prompt in terminal frames (fix by @bcc32)Beyond what's showcased, I've poured much love and effort into polishing the agent-shell experience. Interested in the nitty-gritty? Have a look through my regular commits.
If agent-shell is useful to you, please consider sponsoring the project. These days, I've been working on agent-shell daily.

LLM tokens aren't free, and neither is the time dedicated to building this stuff. While I now have more time to work on agent-shell as an indie dev, I also have bills to pay ;)
Unless I can make this work sustainable, I will have to shift my focus to work on something else that is.
✨ Sponsor agent-shell ✨-1:-- agent-shell 0.47 updates (Post Alvaro Ramirez)--L0--C0--2026-03-12T00:00:00.000Z
I am currently reading Clojure for the brave and true by Daniel Higginbotham and in Chapter 2 about how to use Emacs for Clojure, there are instructions on how to set up Emacs with Cider, a Clojure REPL. I tried installing the package with a use-package configuration with :ensure t and I got the package. However, when I tried using it as the book instructed, I just got an error. I then realised the need for OpenJDK and Clojure and installed those with my home-config.scm file in Guix. I tried again and I still got the same error. I then tried installing emacs-cider from Guix as well with the hope that maybe there was some kind of incompatibility between OpenJDK, Clojure and Cider that was ironed out in the guix package. It still did not work.
I tried looking up the error and found a blog post that suggested installing OpenJDK non-Guixily by downloading a binary and adding it to the $PATH which solved the problem for that user. I did not really want this, since I really like the idea of installing my whole system with a scheme file or two in a reproducible way. (I'm not there yet, but I hope to get there over time. I just started using Guix as my main distro a few weeks ago. For now, after installing Guix System or Guix on another distro, I do some manual work and then run a Shell script to set up a few folders and run the command that applies my home-config.)
Tonight, a day later, I DuckDuckWent again and found another blog post about Clojure that told me that since there are three outputs for the openjdk package, to get everything I would need for Clojure, I should install openjdk:jdk, not simply openjdk which would give me openjdk:out. In addition, I also added openjdk:doc since I am a fan of having as much documentation as possible available on my local machine. I saw a video where that documentation was read from inside Cider a couple of days ago, so I thought it might come in handy. Come to think of it, maybe that was accessed over the internet, not from the local machine. Anyway, it doesn't hurt to have some documentation.
And now, everything just works. Turns out the problem was just user error. Whenever there are multiple outputs of a package in Guix, it is important to know what you get with each output and choose accordingly. As someone who just recently switched my main machine to Guix, this is very useful information that I had not seen before. I thought I would write this up in a blog post so people out there trying to fix the same problem have a chance of finding a solution faster than I did, and for my own future reference.
-1:-- Fix error with Cider Clojure REPL in Emacs on Guix (Post Einar Mostad)--L0--C0--2026-03-11T21:12:00.000Z
Over at The Art of Not Asking Why, JTR has a nice post on attaching an image file to an Org file and displaying it inline. He begins by reviewing how to display an image file inline in an Org file and how to attach any file to an Org file. Most experienced Org users are familiar with all this. I used them just the other day to attach a delivery receipt for my tax papers to my accountant.
It’s the second half of his post that’s interesting. It turns out that when you attach a file to an Org file, the link to the attached file is added to org-stored-links just as if you had run org-store-link (bound to Ctrl+c l by default). That means you can insert the image in line by simply running org-insert-link (bound to Ctrl+c Ctrl+l by default) as usual.
Take a look at JTR’s post for all the details. If you often attach image files to an Org file and want to display them inline as well, this workflow is easy and efficient.
-1:-- Displaying Attached Image Files (Post Irreal)--L0--C0--2026-03-11T16:39:18.000Z
I got inspired to look into this topic after receiving the following obscure bug report for neocaml:
I had your package installed. First impressions were good, but then I had to uninstall it. The code formatting on save stopped working for some reason, and the quickest solution was to revert to the previous setup.
I found this somewhat entertaining – neocaml is a major mode, it has nothing to do with code formatting. But I still started to wonder what kind of setup that user might have. So here we are!
Code formatting is one of those things that shouldn’t require much thought – you pick a formatter, you run it, and your code looks consistent. In practice, Emacs gives you a surprising number of ways to get there, from built-in indentation commands to external formatters to LSP-powered solutions. This article covers the landscape and helps you pick the right approach.
One thing to note upfront: most formatting solutions hook into saving the buffer,
but there are two distinct patterns. The more common one is format-then-save
(via before-save-hook) – the buffer is formatted before it’s written to disk,
so the file is always in a formatted state. The alternative is save-then-format
(via after-save-hook) – the file is saved first, then formatted and saved
again. The second approach can be done asynchronously (the editor doesn’t block),
but it means the file is briefly unformatted on disk. Keep this distinction in
mind as we go through the options.
Let’s get the terminology straight. Emacs has excellent built-in indentation
support, but indentation is not the same as formatting. indent-region (C-M-\)
adjusts leading whitespace according to the major mode’s rules. It won’t
reformat long lines, reorganize imports, add or remove blank lines, or apply any
of the opinionated style choices that modern formatters handle.
That said, for many languages (especially Lisps), indent-region on the whole
buffer is all the formatting you’ll ever need:
;; A simple indent-buffer command (Emacs doesn't ship one)
(defun indent-buffer ()
"Indent the entire buffer."
(interactive)
(indent-region (point-min) (point-max)))
Tip: whitespace-cleanup is a nice complement – it handles trailing
whitespace, mixed tabs/spaces, and empty lines at the beginning and end of the
buffer. Adding it to before-save-hook keeps things tidy:
(add-hook 'before-save-hook #'whitespace-cleanup)
The simplest way to run an external formatter is shell-command-on-region
(M-|). With a prefix argument (C-u M-|), it replaces the region with the
command’s output:
C-u M-| prettier --stdin-filepath foo.js RET
(The --stdin-filepath flag doesn’t read from a file – it just tells Prettier
which parser to use based on the filename extension.)
You can wrap this in a command for repeated use:
(defun format-with-prettier ()
"Format the current buffer with Prettier."
(interactive)
(let ((point (point)))
(shell-command-on-region
(point-min) (point-max)
"prettier --stdin-filepath foo.js"
(current-buffer) t)
(goto-char point)))
This works, but it’s fragile – no error handling, no automatic file type detection, and cursor position is only approximately preserved. For anything beyond a quick one-off, you’ll want a proper package.
reformatter.el is a small library that generates formatter commands from a simple declaration. You define the formatter once, and it creates everything you need:
(reformatter-define black-format
:program "black"
:args '("-q" "-")
:lighter " Black")
This single form generates three things:
black-format-buffer – format the entire bufferblack-format-region – format the selected regionblack-format-on-save-mode – a minor mode that formats on saveEnabling format-on-save is then just:
(add-hook 'python-mode-hook #'black-format-on-save-mode)
reformatter.el handles temp files, error reporting, and stdin/stdout piping.
It also supports formatters that work on files instead of stdin (via :stdin nil
and :input-file), and you can use buffer-local variables in :program and
:args for per-project configuration via .dir-locals.el.
I love the approach taken by this package! It’s explicit, you see exactly what’s being called, and the generated on-save mode plays nicely with the rest of your config.
format-all takes a different approach – it auto-detects the right formatter for 70+ languages based on the major mode. You don’t define anything; it just works:
(add-hook 'prog-mode-hook #'format-all-mode)
(add-hook 'prog-mode-hook #'format-all-ensure-formatter)
The main command is format-all-region-or-buffer. The format-all-mode minor
mode handles format-on-save. If you need to override the auto-detected formatter,
set format-all-formatters (works well in .dir-locals.el):
;; In .dir-locals.el -- use black instead of autopep8 for Python
((python-mode . ((format-all-formatters . (("Python" black))))))
;; Or in your init file
(setq-default format-all-formatters '(("Python" black)))
The trade-off is less control – you’re trusting the package’s formatter database, and debugging issues is harder when you don’t see the underlying command.
apheleia is the most sophisticated option. It solves two problems the other packages don’t:
;; Enable globally
(apheleia-global-mode +1)
apheleia auto-detects formatters like format-all, but you can configure things
explicitly:
;; Chain multiple formatters (e.g., sort imports, then format)
(setf (alist-get 'python-mode apheleia-mode-alist)
'(isort black))
Formatter chaining is a killer feature – isort then black, eslint then
prettier, etc. No other package handles this as cleanly.
Caveat: Because apheleia formats after save, the file on disk is briefly in an unformatted state. This is usually fine, but it can confuse tools that watch files for changes. It also doesn’t support TRAMP/remote files.
If you’re already using a language server, formatting is built in. The language server handles the formatting logic, and Emacs just sends the request.
The main commands are eglot-format (formats the active region, or the entire
buffer if no region is active) and eglot-format-buffer (always formats the
entire buffer).
Format-on-save requires a hook – eglot doesn’t provide a toggle for it:
(add-hook 'eglot-managed-mode-hook
(lambda ()
(add-hook 'before-save-hook #'eglot-format-buffer nil t)))
The nil t makes the hook buffer-local, so it only fires in eglot-managed
buffers.
The equivalents here are lsp-format-buffer and lsp-format-region.
lsp-mode has a built-in option for format-on-save:
(setq lsp-format-buffer-on-save t)
It also supports on-type formatting (formatting as you type closing braces,
semicolons, etc.) via lsp-enable-on-type-formatting, which is enabled by
default.
gopls or rust-analyzer) have excellent formatters; others may not
support formatting at all..clang-format,
pyproject.toml, .prettierrc, etc. This is actually a feature if you’re
working on a team, since the config is shared.There’s no single right answer, but here’s a rough guide:
indent-region is probably
all you need.reformatter.el – explicit, simple,
and predictable.format-all or apheleia. Pick
apheleia if you want async formatting and cursor stability.eglot-format / lsp-format-buffer. One less
package to maintain.reformatter.el for others. Just be careful not to have two things
fighting over format-on-save for the same mode.Tip: Whichever approach you choose, consider enabling format-on-save per
project via .dir-locals.el rather than globally. Not every project uses the
same formatter (or any formatter at all), and formatting someone else’s
unformatted codebase on save is a recipe for noisy diffs.
;; .dir-locals.el
((python-mode . ((eval . (black-format-on-save-mode)))))
So many options, right? That’s so Emacs!
I’ll admit that I don’t actually use any of the packages mentioned in this
article – I learned about all of them while doing a bit of research for
alternatives to the DIY and LSP approaches. That said, I have a very high
opinion of everything done by Steve Purcell
(author of reformatter.el, many other Emacs packages, a popular Emacs
Prelude-like config, and co-maintainer of
MELPA) and Radon Rosborough (author of
apheleia, straight.el, and the
Radian Emacs config), so I have no
issue endorsing packages created by them.
I’m in camp LSP most of the time these days, and I’d guess most people are too.
But if I weren’t, I’d probably take apheleia for a spin. Either way, it’s
never bad to have options, right?
There are languages where LSP isn’t as prevalent – all sorts of Lisp dialects,
for instance – where something like apheleia or
reformatter.el might come in handy. But then again, in Lisps indent-region
works so well that you rarely need anything else. I’m a huge fan of
indent-region myself – for any good Emacs mode, it’s all the formatting you
need.
-1:-- Code Formatting in Emacs (Post Emacs Redux)--L0--C0--2026-03-11T08:30:00.000Z
-1:-- 2026-02 Austin Emacs Meetup (Post Eric MacAdie)--L0--C0--2026-03-11T06:37:02.000Z
In the previous article in the Emacs ways series we did a little jive on copying files from one source location to a destination location. As you can imagine, this keeps a copy in the source location and creates a copy in the destination location—exactly what you would expect.
Now, what if you don’t want a copy in the source location, but want to move that file completely to a different location? Or, what if you wanted to keep the source file in place but merely change the name?
This is where the UNIXy way of doing things can be a little confusing, and I think the Emacs way is superior, but I will let you be the judge of that.
If you are a writer interested in introducing open source tools into your workflow, you will love my DRM-free eBooks:
Now let’s get into some Emacs fun.
If you are coming over the world of GUI file managers, you likely think about renaming and moving files as two separate operations. However, on your UNIXy command line, renaming and moving are both covered by mv command.
mv old_name.txt new_name.txt mv file.txt /path/to/destination/ mv -v *.org ~/documents/
Example output:
$ mv -v old_name.txt new_name.txt renamed 'old_name.txt' -> 'new_name.txt' $ mv -v report.txt ~/documents/ renamed 'report.txt' -> '/home/user/documents/report.txt' $ mv -v *.org ~/documents/ renamed 'notes.org' -> '/home/user/documents/notes.org' renamed 'todo.org' -> '/home/user/documents/todo.org'
In your Emacs text editor, however, moving and renaming are still covered under one action, but keep the identification of “renaming” rather than “moving.” Let’s take a look at how this works.
In dired, position your cursor point on the file you want to rename and press R.
Or, interactively, you can do M-x rename-file.
For regexp-based renaming there is also % R.
Example dired interaction (renaming):
/home/user/projects: -rw-r--r-- 1 user user 1234 Jan 8 14:22 draft.txt Rename draft.txt to: final.txt
Example regexp rename (% R):
Rename from (regexp): \.txt$ Rename to: .org /home/user/notes: * -rw-r--r-- 1 user user 1234 Jan 8 14:22 ideas.txt * -rw-r--r-- 1 user user 5678 Jan 7 09:15 plans.txt Rename ideas.txt to ideas.org? (y or n) y Rename plans.txt to plans.org? (y or n) y
The regexp rename is particularly powerful for batch operations, matching the flexibility of shell globbing but with interactive confirmation for each file.
The post The Emacs Way: Moving and Renaming Files appeared first on Chris Maiorana.
-1:-- The Emacs Way: Moving and Renaming Files (Post Chris Maiorana)--L0--C0--2026-03-11T04:00:00.000Z
Writing quizzes in Canvas is, to put it mildly, a pain in the neck, especially for keyboard-centric users. Every tiny step in the process requires a mouse click, something that quickly becomes irritating. Fortunately, the New York Institute of Technology provides the Canvas Exam Converter which takes a formatted text file and converts to an XML file that Canvas can import. This means that, with a little help from Emacs, writing quizzes that can be quickly imported into Canvas is quick and easy. (See my 2024 post, Converting Org Files to Canvas Quizzes for details.)
Coming up with good questions is still a challenge, though. It turns out that Claude is very effective at writing good multiple choice questions from a document. The questions are formatted like this:
**Question 1:** According to Aristotle, which of the following best describes distributive justice?
- A) Restoring equality after a wrong has been committed
- B) Equality of ratios based on factors like merit, need, or virtue
- C) Following the laws of society without exception
- D) Maximizing overall welfare for the greatest number of people
---
**Question 2:** Which of the following is a criticism of the Utilitarian approach to punishment?
- A) It fails to consider the consequences of punishment
- B) It relies too heavily on retribution
- C) It can justify punishing the innocent if doing so maximizes good outcomes
- D) It requires the punishment to exactly fit the crime
To run my conversion function for sending to NYIT, I first need it in this form:
1. According to Aristotle, which of the following best describes distributive justice?
a) Restoring equality after a wrong has been committed
b) Equality of ratios based on factors like merit, need, or virtue
c) Following the laws of society without exception
d) Maximizing overall welfare for the greatest number of people
2. Which of the following is a criticism of the Utilitarian approach to punishment?
a) It fails to consider the consequences of punishment
b) It relies too heavily on retribution
c) It can justify punishing the innocent if doing so maximizes good outcomes
d) It requires the punishment to exactly fit the crime
For weeks, I’ve been converting them a series of regex search and replace operations, but why do that every time? One of the main reasons I use Emacs is to avoid repetitive, tedious tasks. Here’s the function:
(defun convert-quiz-claude-to-org ()
"Convert markdown-style quiz questions from Claude to Org mode format. Operates on the current buffer or active region."
(interactive)
(let ((start (if (use-region-p) (region-beginning) (point-min)))
(end (if (use-region-p) (region-end) (point-max)))
(question-num 0))
(save-excursion
;; Remove '---' separator lines
(goto-char start)
(while (re-search-forward "^---\n?" end t)
(replace-match ""))
;; Convert **Question N:** to "N."
(goto-char start)
(while (re-search-forward "^\\*\\*Question [0-9]+:\\*\\* " end t)
(setq question-num (1+ question-num))
(replace-match (format "%d. " question-num)))
;; Convert "- A)" "- B)" etc. to " a)" " b)" etc.
(goto-char start)
(while (re-search-forward "^- \\([A-D]\\))" end t)
(replace-match (format " %s)" (downcase (match-string 1)))))
;; Remove blank lines
(goto-char start)
(while (re-search-forward "^[[:blank:]]*\n" end t)
(replace-match "")))))
-1:-- Convert Claude Quizzes to Emacs Org Mode (Post Randy Ridenour)--L0--C0--2026-03-11T00:32:00.000Z
During the sessions with my French tutor, I share a Google document so that we can mark the words where I need to practice my pronunciation some more or tweak the wording. Using Ctrl+B to make the word as bold is an easy way to make it jump out.
I used to copy these changes into my Org Mode notes manually, but today I thought I'd try automating some of it.
First, I need a script to download the HTML for a specified Google document. This is probably easier to do with the NodeJS library rather than with oauth2.el and url-retrieve-synchronously because of various authentication things.
require('dotenv').config();
const { google } = require('googleapis');
async function download(fileId) {
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/drive.readonly'],
});
const drive = google.drive({ version: 'v3', auth });
const htmlRes = await drive.files.export({
fileId: fileId,
mimeType: 'text/html'
});
return htmlRes.data;
}
async function main() {
console.log(await download(process.argv.length > 2 ? process.argv[2] : process.env['DOC_ID']));
}
main();
Then I can wrap a little bit of Emacs Lisp around it.
(defvar my-google-doc-download-command
(list "nodejs" (expand-file-name "~/bin/download-google-doc-html.cjs")))
(defun my-google-doc-html (doc-id)
(when (string-match "https://docs\\.google\\.com/document/d/\\(.+?\\)/" doc-id)
(setq doc-id (match-string 1 doc-id)))
(with-temp-buffer
(apply #'call-process (car my-google-doc-download-command)
nil t nil (append (cdr my-google-doc-download-command) (list doc-id)))
(buffer-string)))
(defun my-google-doc-clean-html (html)
"Remove links on spaces, replace Google links."
(let ((dom (with-temp-buffer
(insert html)
(libxml-parse-html-region))))
(dom-search
dom
(lambda (o)
(when (eq (dom-tag o) 'a)
(when (and (dom-attr o 'href)
(string-match "https://\\(www\\.\\)?google\\.com/url\\?q=" (dom-attr o 'href)))
(let* ((parsed (url-path-and-query
(url-generic-parse-url (dom-attr o 'href))))
(params (url-parse-query-string (cdr parsed))))
(dom-set-attribute o 'href (car (assoc-default "q" params #'string=)))))
(let ((text (string= (string-trim (dom-text o)) "")))
(when (string= text "")
(setf (car o) 'span))))
(when (and
(string-match "font-weight:700" (or (dom-attr o 'style) ""))
(not (string-match "font-style:normal" (or (dom-attr o 'style) ""))))
(setf (car o) 'strong))
(when (dom-attr o 'style)
(dom-remove-attribute o 'style))))
;; bold text is actually represented as font-weight:700 instead
(with-temp-buffer
(svg-print dom)
(buffer-string))))
(defun my-google-doc-org (doc-id)
"Return DOC-ID in Org Mode format."
(pandoc-convert-stdio (my-google-doc-clean-html (my-google-doc-html doc-id)) "html" "org"))
I have lots of sections in that document, including past journal entries, so I want to get a specific section by name.
(defun my-org-get-subtree-by-name (org-text heading-name)
"Return ORG-TEXT subtree for HEADING-NAME."
(with-temp-buffer
(insert org-text)
(org-mode)
(goto-char (point-min))
(let ((org-trust-scanner-tags t))
(car (delq nil
(org-map-entries
(lambda ()
(when (string= (org-entry-get (point) "ITEM") heading-name)
(buffer-substring (point) (org-end-of-subtree))))))))))
Now I can get the bolded words from a section of my notes, with just a sentence for context. I use pandoc to convert it to Org Mode syntax.
(defvar my-lang-words-for-review-context-function 'sentence-at-point)
(defvar my-lang-tutor-notes-url nil)
(defun my-lang-tutor-notes (section-name)
(my-org-get-subtree-by-name
(my-google-doc-org my-lang-tutor-notes-url)
section-name))
(defun my-lang-words-for-review (section)
"List the bolded words for review in SECTION."
(let* ((section (my-lang-tutor-notes section))
results)
(with-temp-buffer
(insert section)
(org-mode)
(goto-char (point-min))
(org-map-entries
(lambda ()
(org-end-of-meta-data t)
(while (re-search-forward "\\*[^* ].*?\\*" nil t)
(cl-pushnew
(replace-regexp-in-string
"[ \n ]+" " "
(funcall my-lang-words-for-review-context-function))
results
:test 'string=)))))
(nreverse results)))
For example, when I run it on my notes on artificial intelligence, this is the list of bolded words and the sentences that contain them.
(my-lang-words-for-review "Sur l'intelligence artificielle")
I can then go into the WhisperX transcription JSON file and replay those parts for closer review.
I can also tweak the context function to give me less information. For example, to limit it to the containing phrase, I can do this:
(defun my-split-string-keep-delimiters (string delimiter)
(when string
(let (results pos)
(with-temp-buffer
(insert string)
(goto-char (point-min))
(setq pos (point-min))
(while (re-search-forward delimiter nil t)
(push (buffer-substring pos (match-beginning 0)) results)
(setq pos (match-beginning 0)))
(push (buffer-substring pos (point-max)) results)
(nreverse results)))))
(ert-deftest my-split-string-keep-delimiters ()
(should
(equal (my-split-string-keep-delimiters
"Beaucoup de gens ont une réaction forte contre l'IA pour plusieurs raisons qui *incluent* le battage médiatique excessif dont elle fait l'objet, son utilisation à mauvais escient, et *l'inondation de banalité* qu'elle produit."
", \\| que \\| qui \\| qu'ils? \\| qu'elles? \\| qu'on "
)
)))
(defun my-lang-words-for-review-phrase-context (&optional s)
(setq s (replace-regexp-in-string " " " " (or s (sentence-at-point))))
(string-join
(seq-filter (lambda (s) (string-match "\\*" s))
(my-split-string-keep-delimiters s ", \\| parce que \\| que \\| qui \\| qu'ils? \\| qu'elles? \\| qu'on \\| pour "))
" ... "))
(ert-deftest my-lang-words-for-review-phrase-context ()
(should
(equal (my-lang-words-for-review-phrase-context
"Je peux consacrer une petite partie de mon *budget* à des essais, mais je ne veux pas travailler davantage pour rentabiliser une dépense plus importante.")
"Je peux consacrer une petite partie de mon *budget* à des essais")))
(let ((my-lang-words-for-review-context-function 'my-lang-words-for-review-phrase-context))
(my-lang-words-for-review "Sur l'intelligence artificielle"))
Now that I have a function for retrieving the HTML or Org Mode for a section, I can use that to wdiff against my current text to more easily spot wording changes.
(defun my-lang-tutor-notes-wdiff-org ()
(interactive)
(let ((section (org-entry-get (point) "ITEM")))
(my-wdiff-strings
(replace-regexp-in-string
" " " "
(my-org-subtree-text-without-blocks))
(replace-regexp-in-string
" " " "
(my-lang-tutor-notes section)))))
Related:
my-wdiff-strings is in Wdiffmy-org-subtree-text-without-blocks is in Counting words without blocksScreenshot:
You can e-mail me at sacha@sachachua.com.
-1:-- Emacs Lisp and NodeJS: Getting the bolded words from a section of a Google Document (Post Sacha Chua)--L0--C0--2026-03-10T18:21:24.000Z
I loyally enjoy running Debian Stable. I am with less enthusiasm accustomed to GNOME, after years of habit and customizations on which I have come to rely. But Debian has some disadvantages: for me, it was that (apparently) GRUB was not always configured correctly after installation to a random laptop. Meanwhile Linux Mint (inlcuding LMDE) installs are always solid. And I am too thick to troubleshoot GRUB. I found that I can have the best of both worlds.
Linux Mint is a popular and highly-recommended Linux distribution based on Ubuntu. I avoid Ubuntu, although Linux Mint overcomes many of my objections (by removing snaps and other Canonical transgressions). But there is also a Linux Mint Debian Edition (LMDE) in which Linux Mint user affordances, including its Cinnamon desktop environment, are built atop Debian instead. (For the record, I install Linux Mint on machines that I give to people who want to try Linux. Everything just works, reliably, and Cinnamon is a great entry point for Windows refugees, with a nice familiar look and feel.)
To my surprise, it turns out that it is possible to completely remove Cinnamon and its dependencies from LMDE and replace it with GNOME. The resulting experience is very similar to a Debian GNOME install, with the following differences (that I take to be advantages):
Start with a fresh install of Linux Mint Debian Edition (LMDE 7 “Gigi”) based on Debian 13 “Trixie.”
Open a terminal and bring everything up to date. This could take awhile, but it cannot be skipped (I checked). I didn’t bother to try to determine which dependencies are important.
apt update apt upgrade reboot
tasksel.
The most reliable way to install GNOME is via tasksel.
sudo apt install tasksel sudo tasksel
From the tasksel menu:
Choose software to install: [*] Debian desktop environment [*] ... GNOME [ ] ... Xfce [ ] ... GNOME Flashback [ ] ... KDE Plasma [ ] ... Cinnamon [ ] ... MATE [ ] ... LXDE [ ] ... LXQt
select both Debian desktop environment and GNOME.
Another menu will appear for choosing a “display” (login) manager. I prefer gdm3 because it follows GNOME customization settings.
Default display manager:
gdm3
lightdm
Reboot again. Note that login is now via gdm3 and that your default login is now into GNOME.
You certainly could stop here and keep Cinnamon installed. You could also leave all the Linux Mint default Cinnamon apps installed. But I don’t want any of this stuff. The mint* packages are cosmetic user-interface stuff that customizes Cinnamon.
sudo apt autoremove --purge cinnamon* mint*
After that last step, log out and log in again. Note: some icons might be missing in e.g. Nautilus windows; use GNOME Tweaks (sudo apt install gnome-tweaks)
Appearance → Styles → Icons → Adwaita (default) to restore the GNOME defaults.
I was surprised that I could remove all of the cinnamon* and mint* packages from LMDE without breaking anything. And then to get what appears to be a nice stock Debian/GNOME environment, with the benefits of Linux Mint.
-1:-- Replacing Cinnamon with GNOME on Linux Mint Debian Edition (Post James Endres Howell)--L0--C0--2026-03-10T16:34:00.000Z
This is my entry to the march emacs carnival on mistakes and misconceptions, hosted by Philip Kaluđerčić.
One of the things that hindered my usage of computers for a long time is that i thought that it was somehow better to use the keyboard as much as possible. This mentality is rampant among linux users in particular, and the majority of the classic linux/unix tools were designed for use without a mouse (because mouses were not commonplace at the time that they were created).
Many classic linux programs prefer to be operated with the keyboard, and emacs is no exception. But emacs is rather special in that it also has an extremely functional gui, and that gui is the default way of using emacs if a windowing system is available for it. There is good support for smooth scrolling, proportional fonts, images, and various other sundries associated with life after the eighties, including mouse support.
Unfortunately, there is still the notion among many people that emacs is not supposed to be used with the mouse: The guru-mode package, for example, claims to help you “learn to use emacs the way it was meant to be used”. The arguments put forth by these people had me convinced for a good while, and i cannot remember exactly why i changed my mind. But now my question is: if emacs was not meant to be used with the mouse, why is its mouse support so good?
There are all sorts of arguments for and against the mouse when it comes to speed. I won’t contribute to that discussion, and i would encourage others to also be sceptical of the conclusions those studies come to, as they tend to have some kind of bias towards a particular outcome. What i will point everyone towards is this blog post, which argues that calm, not speed, should be the focus, and the mouse, for the author, is the calmer input.
I no longer use avy, or any other packages aiming to replace mouse functionality with keyboard movements. It’s not that i think they don’t have their place, but rather that using the mouse is significantly less cognitive burden, and not significantly more effort.
Perhaps i should also note that although i say mouse, most of the time i’m actually using a trackpad. I used to use a thinkpad, and then i’d use the trackpoint. All have the same benefits over a keyboard for their particular use case: using a device designed for navigating a point across a plane in two dimensions works really nicely when i want to navigate a point across a plane in two dimensions.
One of the nicest things about emacs is that although it is very evidently a historical program, it still has added many modern features over the years. I know multiple people who are very excited about emacs’s integration with llms, although i admit that personally i can’t muster the same excitement that they have. But that doesn’t really matter: what matters is that emacs has a single fundamental concept, that of the text buffer, and as a result, anything that reads text and writes text can be integrated well with the platform. However, although the text buffer lends itself very well to keyboard interaction—i cannot imagine myself ever using a virtual keyboard to write this article, for example—the keyboard doesn’t need to be the only method of interaction. As apple showed us with the mac over forty years ago, the mouse is a powerful input device with many benefits when used in conjunction with the keyboard.
Eschewing the mouse purely because of some misguided sense of purity or superiority means that the emacs user is making some tasks more difficult than the need to be with entirely arbitrary justifications. It’s not necessary to use the mouse for everything, and i still use the keyboard to jump around text all the time. But there are some situations where the mouse feels like the best tool for the job, and i am glad that i am now able to take advantage of that best tool guilt-free.
-1:-- You don't not need the mouse (Post noa ks)--L0--C0--2026-03-10T16:00:00.000Z
I recently wrote about
customizing font-lock in the age of Tree-sitter.
After publishing that article, a reader pointed out that I’d overlooked
font-lock-ignore – a handy option for selectively disabling font-lock rules
that was introduced in Emacs 29. I’ll admit I had no idea it existed, and I
figured if I missed it, I’m probably not the only one.1
It’s a bit amusing that something this useful only landed in Emacs 29 – the very release that kicked off the transition to Tree-sitter. Better late than never, right?
Traditional font-lock gives you two ways to control highlighting: the coarse
font-lock-maximum-decoration (pick a level from 1 to 3) and the surgical
font-lock-remove-keywords (manually specify which keyword rules to drop). The
first is too blunt – you can’t say “I want level 3 but without operator
highlighting.” The second is fragile – you need to know the exact internal
structure of the mode’s font-lock-keywords and call it from a mode hook.
What was missing was a declarative way to say “in this mode, don’t highlight
these things” without getting your hands dirty with the internals. That’s
exactly what font-lock-ignore provides.
font-lock-ignore is a single user option (a defcustom) whose value is an
alist. Each entry maps a mode symbol to a list of conditions that describe which
font-lock rules to suppress:
(setq font-lock-ignore
'((MODE CONDITION ...)
(MODE CONDITION ...)
...))
MODE is a major or minor mode symbol. For major modes, derived-mode-p is
used, so a rule for prog-mode applies to all programming modes. For minor
modes, the rule applies when the mode is active.
CONDITION can be:
font-lock-*-face matches all standard font-lock faces."TODO" or "defun".(pred FUNCTION) – suppresses rules for which FUNCTION returns non-nil.(not CONDITION), (and CONDITION ...), (or CONDITION ...) – the
usual logical combinators.(except CONDITION) – carves out exceptions from broader rules.Note: The Emacs manual covers font-lock-ignore in the
Customizing Keywords
section of the Elisp reference.
font-lock-ignore is most useful when you’re generally happy with a mode’s
highlighting but want to tone down specific aspects. Maybe you find type
annotations too noisy, or you don’t want preprocessor directives highlighted, or
a minor mode is adding highlighting you don’t care for.
For Tree-sitter modes, the feature/level system described in my
previous article
is the right tool for the job. But for traditional modes – and there are still
plenty of those – font-lock-ignore fills a gap that existed for decades.
To use font-lock-ignore effectively, you need to know which faces are being
applied to the text you want to change. A few built-in commands make this easy:
C-u C-x = (what-cursor-position with a prefix argument) – the quickest
way. It shows the face at point along with other text properties right in the
echo area.M-x describe-face – prompts for a face name (defaulting to the face at
point) and shows its full definition, inheritance chain, and current
appearance.M-x list-faces-display – opens a buffer listing all defined faces with
visual samples. Handy for browsing the font-lock-*-face family and the newer
Emacs 29 faces like font-lock-bracket-face and font-lock-operator-face.Once you’ve identified the face, just drop it into font-lock-ignore.
Here’s the example from the Emacs manual, which shows off the full range of conditions:
(setq font-lock-ignore
'((prog-mode font-lock-*-face
(except help-echo))
(emacs-lisp-mode (except ";;;###autoload"))
(whitespace-mode whitespace-empty-at-bob-regexp)
(makefile-mode (except *))))
Let’s break it down:
prog-mode derivatives, suppress all standard font-lock-*-face
highlighting (syntactic fontification for comments and strings is unaffected,
since that uses the syntax table, not keyword rules).help-echo text property.emacs-lisp-mode, also keep the ;;;###autoload cookie highlighting
(which rule 1 would have suppressed).whitespace-mode is active, additionally suppress the
whitespace-empty-at-bob-regexp highlight.makefile-mode, (except *) undoes all previous conditions, effectively
exempting Makefiles from any filtering.Here are some simpler, more focused examples:
;; Disable type highlighting in all programming modes
(setq font-lock-ignore
'((prog-mode font-lock-type-face)))
;; Disable bracket and operator faces specifically
(setq font-lock-ignore
'((prog-mode font-lock-bracket-face
font-lock-operator-face)))
;; Disable keyword highlighting in python-mode only
(setq font-lock-ignore
'((python-mode font-lock-keyword-face)))
Pretty sweet, right?
A few things to keep in mind:
font-lock-ignore only affects keyword fontification (the regexp-based
rules in font-lock-keywords). It does not touch syntactic fontification –
comments and strings highlighted via the syntax table are not affected.font-lock-compile-keywords),
changes take effect the next time font-lock is initialized in a buffer. If
you’re experimenting, run M-x font-lock-mode twice (off then on) to see
your changes.I don’t know about you, but I really wish that font-lock-ignore got added to
Emacs a long time ago. Still, the transition to Tree-sitter modes is bound to
take years, so many of us will still get to leverage font-lock-ignore and
benefit from it.
That’s all I have for you today. Keep hacking!
That’s one of the reasons I love writing about Emacs features – I often learn something new while doing the research for an article, and as bonus I get to learn from my readers as well. ↩
-1:-- Taming Font-Lock with font-lock-ignore (Post Emacs Redux)--L0--C0--2026-03-10T06:11:00.000Z
If you use kubernetes-el, don't update for now, and you might want to check your installation if you updated it recently. The repo was compromised a few days ago.
I've occasionally wanted to tangle a single Org Mode source block to multiple places, so I'm glad to hear that ob-tangle has just added support for multiple targets. Niche, but could be handy. I'm also curious about using clime to write command-line tools in Emacs Lisp that handle argument parsing and all the usual stuff.
If you're looking for something to write about, why not try this month's Emacs Carnival theme of mistakes and misconceptions?
Enjoy!
Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!
You can comment on Mastodon or e-mail me at sacha@sachachua.com.
-1:-- 2026-03-09 Emacs news (Post Sacha Chua)--L0--C0--2026-03-09T14:45:21.000Z
It’s tough to make predictions, especially about the future.
– Yogi Berra
I’ve been an Emacs fanatic for over 20 years. I’ve built and maintained some of the most popular Emacs packages, contributed to Emacs itself, and spent countless hours tweaking my configuration. Emacs isn’t just my editor – it’s my passion, and my happy place.
Over the past year, I’ve also been spending a lot of time with Vim and Neovim, relearning them from scratch and having a blast contrasting how the two communities approach similar problems. It’s been a fun and refreshing experience.1
And lately, like everyone else in our industry, I’ve been playing with AI tools – Claude Code in particular – watching the impact of AI on the broader programming landscape, and pondering what it all means for the future of programming. Naturally, I keep coming back to the same question: what happens to my beloved Emacs and its “arch nemesis” Vim in this brave new world?
I think the answer is more nuanced than either “they’re doomed” or “nothing changes”. Predicting the future is hard, but speculating is irresistible.
The only thing that is constant is change.
– Heraclitus
Things are rarely black and white – usually just some shade of gray. AI is obviously no exception, and I think it’s worth examining both sides honestly before drawing any conclusions.
Let’s start with the challenges.
VS Code is already the dominant editor by a wide margin, and it’s going to get first-class integrations with every major AI tool – Copilot (obviously), Codex, Claude, Gemini, you name it. Microsoft has every incentive to make VS Code the best possible host for AI-assisted development, and the resources to do it.
On top of that, purpose-built AI editors like Cursor, Windsurf, and others are attracting serious investment and talent. These aren’t adding AI to an existing editor as an afterthought – they’re building the entire experience around AI workflows. They offer integrated context management, inline diffs, multi-file editing, and agent loops that feel native rather than bolted on.
Every developer who switches to one of these tools is a developer who isn’t learning Emacs or Vim keybindings, isn’t writing Elisp, and isn’t contributing to our ecosystems. The gravity well is real.
I never tried Cursor and Windsurf simply because they are essentially forks of VS Code and I can’t stand VS Code. I’ve tried it several times over the years and I never felt productive in it for a variety of reasons.
Part of the case for Emacs and Vim has always been that they make you faster at writing and editing code. The keybindings, the macros, the extensibility – all of it is in service of making the human more efficient at the mechanical act of coding.
But if AI is writing most of your code, how much does mechanical editing speed matter? When you’re reviewing and steering AI-generated diffs rather than typing code character by character, the bottleneck shifts from “how fast can I edit” to “how well can I specify intent and evaluate output.” That’s a fundamentally different skill, and it’s not clear that Emacs or Vim have an inherent advantage there.
The learning curve argument gets harder to justify too. “Spend six months learning Emacs and you’ll be 10x faster” is a tough sell when a junior developer with Cursor can scaffold an entire application in an afternoon.2 Then again, maybe the question of what exactly you’re editing deserves a closer look.
VS Code has Microsoft. Cursor has venture capital. Emacs has… a small group of volunteers and the FSF. Vim had Bram, and now has a community of maintainers. Neovim has a small but dedicated core team.
This has always been the case, of course, but AI amplifies the gap. Building deep AI integrations requires keeping up with fast-moving APIs, models, and paradigms. Well-funded teams can dedicate engineers to this full-time. Volunteer-driven projects move at the pace of people’s spare time and enthusiasm.
Let’s go all the way: what if programming as we know it is fully automated within the next decade? If AI agents can take a specification and produce working, tested, deployed software without human intervention, we won’t need coding editors at all. Not Emacs, not Vim, not VS Code, not Cursor. The entire category becomes irrelevant.
I don’t think this is likely in the near term, but it’s worth acknowledging as a possibility. The trajectory of AI capabilities has surprised even the optimists (and I was initially an AI skeptic, but the rapid advancements last year eventually changed my mind).
That paints a grim picture, but Emacs and Vim have been written off more times than I can count. Eclipse was going to kill them. IntelliJ was going to kill them. VS Code was going to kill them. Sublime Text, Atom, TextMate – all were supposedly the final nail in the coffin. Most of those “killers” are themselves dead or declining, while Emacs and Vim keep chugging along. There’s a resilience to these editors that’s easy to underestimate.
So let’s look at the other side of the coin.
One of the most underappreciated benefits of AI for Emacs and Vim users is mundane: troubleshooting. Both editors have notoriously steep learning curves and opaque error messages. “Wrong type argument: stringp, nil” has driven more people away from Emacs than any competitor ever did.
AI tools are remarkably good at explaining cryptic error messages, diagnosing configuration issues, and suggesting fixes. They can read your init file and spot the problem. They can explain what a piece of Elisp does. They can help you understand why your keybinding isn’t working. This dramatically flattens the learning curve – not by making the editor simpler, but by giving every user access to a patient, knowledgeable guide.
I don’t really need any AI assistance to troubleshoot anything in my Emacs setup, but it’s been handy occasionally in Neovim-land, where my knowledge is relatively modest by comparison.
There’s at least one documented case of someone returning to Emacs after years away, specifically because Claude Code made it painless to fix configuration issues. They’d left for IntelliJ because the configuration burden got too annoying – and came back once AI removed that barrier. “Happy f*cking days I’m home again,” as they put it. If AI can bring back lapsed Emacs users, that’s a good thing in my book.
Something that doesn’t get nearly enough attention: Emacs and Vim have always suffered from the obscurity of their extension languages. Emacs Lisp is a 1980s Lisp dialect that most programmers have never seen before. VimScript is… VimScript. Even Lua, which Neovim adopted specifically because it’s more approachable, is niche enough that most developers haven’t written a line of it.
This has been the single biggest bottleneck for both ecosystems. Not the editors themselves – they’re incredibly powerful – but the fact that customizing them requires learning an unfamiliar language, and most people never make it past copying snippets from blog posts and READMEs.
I felt incredibly overwhelmed by Elisp and VimScript when I was learning Emacs and Vim for the first time, and I imagine I wasn’t the only one. I started to feel very productive in Emacs only after putting in quite a lot of time to actually learn Elisp properly. (Never bothered to do the same for VimScript, though, and I’m not too eager to master Lua either.)
AI changes this overnight. You can now describe what you want in plain English and get working Elisp, VimScript, or Lua. “Write me an Emacs function that reformats the current paragraph to 72 columns and adds a prefix” – done. “Configure lazy.nvim to set up LSP with these keybindings” – done. The extension language barrier, which has been the biggest obstacle to adoption for decades, is suddenly much lower.
After 20+ years in the Emacs community, I often have the feeling that a relatively small group – maybe 50 to 100 people – is driving most of the meaningful progress. The same names show up in MELPA, on the mailing lists, and in bug reports. This isn’t a criticism of those people (I’m proud to be among them), but it’s a structural weakness. A community that depends on so few contributors is fragile.
And it’s not just Elisp and VimScript. The C internals of both Emacs and Vim (and Neovim’s C core) are maintained by an even smaller group. Finding people who are both willing and able to hack on decades-old C codebases is genuinely hard, and it’s only getting harder as fewer developers learn C at all.
AI tools can help here in two ways. First, they lower the barrier for new contributors – someone who understands the concept of what they want to build can now get AI assistance with the implementation in an unfamiliar language. Second, they help existing maintainers move faster. I’ve personally found that AI is excellent at generating test scaffolding, writing documentation, and handling the tedious parts of package maintenance that slow everything down.
The Emacs and Neovim communities aren’t sitting idle. There are already impressive AI integrations:
Emacs:
Neovim:
And this is just a sample. Building these integrations isn’t as hard as it might seem – the APIs are straightforward, and the extensibility of both editors means you can wire up AI tools in ways that feel native. With AI assistance, creating new integrations becomes even easier. I wouldn’t be surprised if the pace of plugin development accelerates significantly.
Funny enough, many of the most powerful AI coding tools are terminal-native. Claude Code, Aider, and various Copilot CLI tools all run in the terminal. And what lives in the terminal? Emacs and Vim.3
Running Claude Code in an Emacs vterm buffer or a Neovim terminal split is
a perfectly natural workflow. You get the AI agent in one pane and your editor
in another, with all your keybindings and tools intact. There’s no context
switching to a different application – it’s all in the same environment.
This is actually an advantage over GUI-based AI editors, where the AI integration is tightly coupled to the editor’s own interface. With terminal-native tools, you get to choose your own editor and your own AI tool, and they compose naturally.
There’s another angle worth considering: if programming is increasingly about writing prompts rather than code, you still benefit from a great text editor for that. Prompts are text, and crafting them well matters. I find it ironic that Claude Code – a tool I otherwise love – doesn’t use readline, so my Emacs keybindings don’t work properly in it, and its vim emulation is fairly poor. I still think using React for CLI apps is a mistake, and I suspect many people would enjoy running Claude Code inside their Emacs or Vim instead. That’s exactly what the Agent Client Protocol (ACP) enables – it lets editors like Emacs (via agent-shell) act as first-class clients for AI agents, giving you proper editing, keybindings, and all the power of your editor while interacting with tools like Claude Code. The best prompt editor might just be the one you’ve been using for decades.
Emacs’s “editor as operating system” philosophy is uniquely well-suited to AI integration. It’s not just a code editor – it’s a mail client (Gnus, mu4e), a note-taking system (Org mode), a Git interface (Magit), a terminal emulator, a file manager, an RSS reader, and much more.
AI can be integrated at every one of these layers. Imagine an AI assistant that can read your org-mode agenda, draft email replies in mu4e, help you write commit messages in Magit, and refactor code in your source buffers – all within the same environment, sharing context. No other editor architecture makes this kind of deep, cross-domain integration as natural as Emacs does.
I stopped using Emacs as my OS a long time ago, and these days
I use it mostly for programming and blogging. (I’m writing this article in Emacs with
the help of markdown-mode.) Still, I’m only one Emacs user and many are probably
using it in a more holistic manner.
Let’s revisit the doomsday scenario. Say programming is fully automated and nobody writes code anymore. Does Emacs die?
Not necessarily. Emacs is already used for far more than programming. People use Org mode to manage their entire lives – tasks, notes, calendars, journals, time tracking, even academic papers. Emacs is a capable writing environment for prose, with excellent support for LaTeX, Markdown, AsciiDoc, and plain text. You can read email, browse the web, manage files, and yes, play Tetris.
Vim, similarly, is a text editing paradigm as much as a program. Vim keybindings have colonized every text input in the computing world – VS Code, IntelliJ, browsers, shells, even Emacs (via Evil mode). Even if the Vim program fades, the Vim idea is immortal.4
And who knows – maybe there’ll be a market for artisanal, hand-crafted software one day, the way there’s a market for vinyl records and mechanical watches.5 “Organic, small-batch code, lovingly typed by a human in Emacs – one character at a time.” I’d buy that t-shirt. And I’m fairly certain those artisan programmers won’t be using VS Code.
So even in the most extreme scenario, both editors have a life beyond code. A diminished one, perhaps, but a life nonetheless.
I think what’s actually happening is more interesting than “editors die” or “editors are fine.” The role of the editor is shifting.
For decades, the editor was where you wrote code. Increasingly, it’s becoming where you review, steer, and refine code that AI writes. The skills that matter are shifting from typing speed and editing gymnastics to specification clarity, code reading, and architectural judgment.
In this world, the editor that wins isn’t the one with the best code completion – it’s the one that gives you the most control over your workflow. And that has always been Emacs and Vim’s core value proposition.
The question is whether the communities can adapt fast enough. The tools are there. The architecture is there. The philosophy is right. What’s needed is people – more contributors, more plugin authors, more documentation writers, more voices in the conversation. AI can help bridge the gap, but it can’t replace genuine community engagement.
Not everyone in the Emacs and Vim communities is enthusiastic about AI, and the objections go beyond mere technophobia. There are legitimate ethical concerns that are going to be debated for a long time:
Energy consumption. Training and running large language models requires enormous amounts of compute and electricity. For communities that have long valued efficiency and minimalism – Emacs users who pride themselves on running a 40-year-old editor, Vim users who boast about their sub-second startup times – the environmental cost of AI is hard to ignore.
Copyright and training data. LLMs are trained on vast corpora of code and text, and the legality and ethics of that training remain contested. Some developers are uncomfortable using tools that may have learned from copyrighted code without explicit consent. This concern hits close to home for open-source communities that care deeply about licensing.
Job displacement. If AI makes developers significantly more productive, fewer developers might be needed. This is an uncomfortable thought for any programming community, and it’s especially pointed for editors whose identity is built around empowering human programmers.
These concerns are already producing concrete action. The Vim community recently saw the creation of EVi, a fork of Vim whose entire raison d’etre is to provide a text editor free from AI-assisted (generated?) code contributions.6 Whether you agree with the premise or not, the fact that people are forking established editors over this tells you how strongly some community members feel.
I don’t think these concerns should stop anyone from exploring AI tools, but they’re real and worth taking seriously. I expect to see plenty of spirited debate about this on emacs-devel and the Neovim issue tracker in the years ahead.
The future ain’t what it used to be.
– Yogi Berra
I won’t pretend I’m not worried. The AI wave is moving fast, the incumbents have massive advantages in funding and mindshare, and the very nature of programming is shifting under our feet. It’s entirely possible that Emacs and Vim will gradually fade into niche obscurity, used only by a handful of diehards who refuse to move on.
What keeps these editors alive isn’t stubbornness – it’s adaptability. The communities are small but passionate, the editors are more capable than ever, and the architecture is genuinely well-suited to the AI era. Vim’s core idea is so powerful that it keeps finding new expression (Neovim being the most vigorous one). And Emacs? Emacs just keeps absorbing whatever the world throws at it. It always has.
The editors that survive won’t be the ones with the flashiest AI features. They’ll be the ones whose users care enough to keep building, adapting, and sharing. That’s always been the real engine of open-source software, and no amount of AI changes that.
So if you’re an Emacs or Vim user: don’t panic, but don’t be complacent either. Learn the new AI tools (if you’re not fundamentally opposed to them, that is). Pimp your setup and make it awesome. Write about your workflows. Help newcomers. The best way to ensure your editor survives the AI age is to make it thrive in it.
Maybe the future ain’t what it used to be – but that’s not necessarily a bad thing.
This essay covered more ground than I originally intended – lots of thoughts have been rattling around in my head for a while now, and I wanted to get them all out. Programming may be hard, but writing prose remains harder. Still, I hope some of these ideas resonated with you.
That’s all I have for you today. Keep hacking!
P.S. There’s an interesting Hacker News discussion about this article. Check it out if you want to see what the broader community thinks!
If you’re curious about my Vim adventures, I wrote about them in Learning Vim in 3 Steps. ↩︎
Not to mention you’ll probably have to put in several years in Emacs before you’re actually more productive than you were with your old editor/IDE of choice. ↩︎
At least some of the time. Admittedly I usually use Emacs in GUI mode, but I always use (Neo)vim in the terminal. ↩︎
Even Claude Code has vim mode. ↩︎
I’m a big fan of mechanical watches myself, so I might be biased here. There’s something deeply satisfying about a beautifully crafted mechanism that doesn’t need a battery or an internet connection to work. ↩︎
-1:-- Emacs and Vim in the Age of AI (Post Bozhidar Batsov)--L0--C0--2026-03-09T08:30:00.000Z
This Thursday, the 12th of March, at 20:00 Europe/Athens time I will do a live presentation of Emacs for OxFLOSS (FLOSS @ Oxford). This is an event organised by people at the University of Oxford. My goal is to introduce Emacs to a new audience by showing them a little of what it can do while describing how exactly it gives users freedom.
The presentation will be about 40 minutes long. I will then answer any questions from the audience. Anyone can participate: no registration is required. The event will be recorded for future reference. The link for the video call and further details are available here: https://ox.ogeer.org/event/computing-in-freedom-with-gnu-emacs-protesilaos-stavrou.
I will prepare a transcript for my talk. This way people can learn about my presentation without having to access the video file.
Looking forward to it!
-1:-- This Thursday I will talk about Emacs @ OxFLOSS (FLOSS @ Oxford) (Post Protesilaos Stavrou)--L0--C0--2026-03-09T00:00:00.000Z
One thing I particularly love about org-mode is org-indent-mode, which
visually indents content under each heading based on its level. It makes long
org files much easier to read and navigate.
Occasionally, I need to edit Markdown files — and every time I do, I miss that clean visual hierarchy. So I vibe-coded a first version over a weekend.
The idea translates naturally from org-indent.el, which ships with Emacs.
Headings marked with # instead of *, same concept.
The first version worked partially, but was full of edge cases: code fences confusing the heading parser, list items indenting wrong, wrapped lines losing alignment. I kept using it day-to-day, tweaking it when something looked off, and simplifying whenever I found a cleaner approach. Eventually it reached a state I was genuinely happy with.
At that point I thought it might be useful to others too, so I decided to share it.
My first instinct was to contribute this directly to markdown-mode, the de-facto Markdown package for Emacs, so all users could get it without installing anything extra. It turns out this is a long-anticipated feature — there have been open issues requesting it for years:
I opened a pull request to add the feature directly into markdown-mode.
The markdown-mode maintainer reviewed the PR and suggested that this would be better as a standalone package since it would be a burden for them to maintain the new feature.
I took the suggestion, refactored the code into its own package:
markdown-indent-mode, and submitted it to MELPA.
Submitting to MELPA was a learning experience in itself. I picked up a few tools along the way for checking Elisp package quality:
checkdoc — checks that docstrings follow Emacs conventionspackage-lint — catches common packaging issuesmelpazoid — an automated checker specifically designed for MELPA submissionsThe MELPA maintainers are volunteers who typically review PRs on weekends, so the process took a few days.
Here's what it does — nothing fancy, just the basics done cleanly:
line-prefix and wrap-prefix text properties for visual indentation# symbols are hidden
This package is available on MELPA. You can use use-package to install it and
add hook to enable it for markdown-mode.
(use-package markdown-indent-mode :hook (markdown-mode . markdown-indent-mode))
Or toggle it on demand with M-x markdown-indent-mode.
| Before | After |
|---|---|
![]() |
![]() |
The source is available at https://github.com/whhone/markdown-indent-mode.
If you ever find yourself editing Markdown in Emacs, give it a try and let me know what you think!
-1:-- Introducing markdown-indent-mode (Post Wai Hon)--L0--C0--2026-03-08T23:30:00.000Z
Suppose you have an org-mode file and want an image to appear in the buffer. The way to do that is to insert a link to the file, for example:
[[home/username/downloads/image.png]].
Then, you toggle inline images with C-c C-x C-v, and the image should display inside the org-mode buffer, provided the path in the link is correct. If you do this often in your notes as I do, you might as well just turn it on for the entire file with #+STARTUP: inlineimages at the top of your org file, with the rest of the options you have there; this way, images will always display when you load the file. This is all nice and good, and most of us org-mode users probably know that.
A common use case for a full workflow like this is attaching images to your org file. You have a file in your Downloads folder, as shown in the example above, and you want to keep the image with your org file where it belongs, rather than in Downloads, where it will be lost among other files sooner or later.
For this, as most of us know, we have org-attach (C-c C-a by default). This starts a wonderful organizational process for our files:
data folder (by default) inside the folder the org-file is in if it’s not thereFor example:
./data/acde070d/8c4c-4f0d-9d8a-162843c10333/someimage.png
If you’re not used to how org-attach works, it might take some time getting used to, but it’s worth it. Images (or any file, as we will deal with soon) are kept next to the files they are associated with. Of course, org-attach is customizable, and you can change those folders and UUIDs to make them less cryptic.
For example, my init includes this:
(setq org-id-method 'ts)
(setq org-attach-id-to-path-function-list
'(org-attach-id-ts-folder-format
org-attach-id-uuid-folder-format))
This tells org-mode to change the UUID to IOS date stamp format, so the folders under the data folder are dates, and tells org-mode to use that system (I wrote about this in length in my old blog; it is yet another post I need to bring over here).
In my case, this creates a file reference system by date: inside the data folder, each month of the year has a folder; inside those, a folder for the day and time (down to fractions of seconds) of the attachment. The beauty of org-attach is that you’re not meant to deal with the files directly. You summon the org-attach dispatcher and tell it to go to the relevant folder (C-c C-a to bring it up, then f as the option to go to that directory).
org-attach and displaying images inline are known to many org-mode users, but here comes the part I never realized:
org-attach stores the link to the file you just attached inside a variable called org-stored-link, along with other links you might have grabbed, like URLs from the web (take a look with C-h v org-stored-links). And, even better, these links are added to your org-insert-link, ready to go when you insert a link to your file with C-c C-l.
So when you have an image ready to attach to an org file, say in your Downloads folder, you could first attach it with org-attach, and then you can call it back quickly with C-c C-l. The trick is, since this is an image link (and not just any file), is not to give it a description. By default, org-mode will suggest you describe the link as the file you attached, but inline images do not work like that, and with a description, the image will just display as a file name. In other words:
A link to an image you want to display in the org buffer should look like:
[[file:/Home/username/downloads/someimage.jpg]]
But any other file would look like:
[[file:/Home/username/downloads/somefile.jpg][description]]
By deleting the suggestion, you are effectively creating the first case, the one that is meant to display images. This is explained nicely here.
There’s more to it. As it turns out, the variable org-attach-store-link-p is responsible for the links to these files to automatically be stored in org-insert-link (you can toggle it to change this option). This is why, when you use it, your files or images will show as [[attachment:description]], without the need for the path as specified above.
I have years of muscle memory to undo, as I’m used to manually inserting the links with the full path for my images. I did not realize the links to the images I’ve attached are right there, ready for me to place into the buffer if I only delete the description.
-1:-- Display images with Org-attach and org-insert-link quickly and effectively (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-03-08T17:43:25.000Z
Howard Abrams has a video and associated post that updates his post on the same subject from 11 years ago. As with yesterday’s post, the video is from EmacsConf 2024 but someone just reposted it to reddit and it’s definitely worth taking a look at.
Abrams is explicit that by “literate programming” he means using code blocks and Babel in Org mode files. It’s an extremely powerful workflow. You can execute those code blocks in place or you can use org-babel-tangle to export the code to a separate file.
The majority of his video and post discuss ways of working with those Org files. One simple example is that he uses the local variables feature of Emacs files to set a hook that automatically tangles the file every time he saves it. That keeps the parent Org file and the generated code file in sync. He also has some functions that leverage Avy to find and execute a code block without changing the point’s location.
Finally, he talks about navigating his—possibly multi-file—projects. We wants to do the usual things like jumping to a function definition or getting a list of where a function is called. Emacs, of course, has functions for that but they don’t work in Org files. So Abrams wrote his own extension to the xref API based on ideas from dumb-jump.
Abrams drew all this together with a hydra that makes it easy for him to call any of his functions. He moves a bit rapidly in the video so you might want to read the post first in order to follow along. The video is 16 minutes, 38 seconds long so plan accordingly.
-1:-- Abrams On Literate Programming Redux (Post Irreal)--L0--C0--2026-03-08T15:42:15.000Z
I've been maintaining Emacs Solo for a while now, and I think it's time to talk about what happened in this latest cycle as the project reaches its two-year mark.
For those who haven't seen it before, Emacs Solo is my daily-driver
Emacs configuration with one strict rule: no external packages.
Everything is either built into Emacs or written from scratch by me in
the lisp/ directory. No package-install, no straight.el, no
use-package :ensure t pointing at ELPA or MELPA. Just Emacs and
Elisp. I'm keeping this post text only, but if you'd like to check how
Emacs Solo looks and feels, the repository has screenshots and more
details.
Why? Partly because I wanted to understand what Emacs actually gives you out of the box. Partly because I wanted my config to survive without breakage across Emacs releases. Partly because I was tired of dealing with package repositories, mirrors going down in the middle of the workday, native compilation hiccups, and the inevitable downtime when something changed somewhere upstream and my job suddenly became debugging my very long (at the time) config instead of doing actual work. And partly, honestly, because it's a lot of fun!
This post covers the recent refactor, walks through every section of the core config, introduces all 35 self-contained extra modules I've written, and shares some thoughts on what I've learned.
Now, I'll be the first to admit: this config is long. But there's a principle behind it. I only add features when they are not already in Emacs core, and when I do, I try to build them myself. That means the code is sketchy sometimes, sure, but it's in my control. I wrote it, I understand it, and when it breaks, I know exactly where to look. The refactor I'm about to describe makes this distinction crystal clear: what is "Emacs core being tweaked" versus what is "a really hacky outsider I built in because I didn't want to live without it".
The single biggest change in this cycle was architectural. Emacs
Solo used to be one big init.el with everything crammed
together. That worked, but it had problems:
— It was hard to navigate (even with outline-mode)
— If someone wanted just one piece, say my Eshell config or my VC extensions, they had to dig through thousands of lines
— It was difficult to tell where "configuring built-in Emacs" ended and "my own hacky reimplementations" began
The solution was clean and simple: split the config into two layers.
init.el (Emacs core configuration)This file configures only built-in Emacs packages and features. Every
use-package block in here has :ensure nil, because it's pointing
at something that ships with Emacs. This is pure, standard Emacs
customization.
The idea is that anyone can read init.el, find a section they like, and
copy-paste it directly into their own config. No dependencies. No
setup. It just works, because it's configuring things Emacs already
has.
lisp/ (Self-contained extra modules)These are my own implementations: replacements for popular external
packages, reimagined as small, focused Elisp files. Each one is a
proper provide/require module. They live under lisp/ and are
loaded at the bottom of init.el via a simple block:
(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory))
(require 'emacs-solo-themes)
(require 'emacs-solo-movements)
(require 'emacs-solo-formatter)
;; ... and so on
If you don't want one of them, just comment out the require line.
If you want to use one in your own config, just copy the .el file
into your own lisp/ directory and require it. That's it.
This separation made the whole project dramatically easier to maintain, understand, and share.
The init.el file is organized into clearly labeled sections (using
outline-mode-friendly headers, so you can fold and navigate them
inside Emacs). Here's every built-in package and feature it touches,
and why.
The emacs use-package block is the largest single section. It sets
up sensible defaults that most people would want:
— Key rebindings: M-o for other-window, M-j for
duplicate-dwim, C-x ; for comment-line, C-x C-b for
ibuffer
— Window layout commands bound under C-x w (these are upcoming
Emacs 31 features: window-layout-transpose,
window-layout-rotate-clockwise, window-layout-flip-leftright,
window-layout-flip-topdown)
— Named frames: C-x 5 l to select-frame-by-name, C-x 5 s to
set-frame-name, great for multi-frame workflows
— Disabling C-z (suspend) because accidentally suspending Emacs in
a terminal is never fun
— Sensible file handling: backups and auto-saves in a cache/
directory, recentf for recent files, clean buffer naming with
uniquify
— Tree-sitter auto-install and auto-mode (treesit-auto-install-grammar t and treesit-enabled-modes t, both Emacs 31)
— delete-pair-push-mark, kill-region-dwim, ibuffer-human-readable-size, all the small quality-of-life settings coming in Emacs 31
A full abbrev-mode setup with a custom placeholder system. You
define abbreviations with ###1###, ###2### markers, and when the
abbreviation expands, it prompts you to fill in each placeholder
interactively. The ###@### marker tells it where to leave point
after expansion. I wrote a whole article about it.
Configures auth-source to use ~/.authinfo.gpg for credential
storage. Simple but essential if you use Gnus, ERC, or any
network-facing Emacs feature.
Makes buffers automatically refresh when files change on disk. Essential for any Git workflow.
Configuration file mode settings and a compilation-mode setup
with ANSI color support, so compiler output actually looks readable.
Custom window management beyond the defaults, because Emacs window management out of the box is powerful but needs a little nudging.
Tab-bar configuration for workspace management. Emacs has had tabs since version 27, and they're genuinely useful once you configure them properly.
Two IRC clients, both built into Emacs, both configured. ERC gets the bigger treatment: logging, scrolltobottom, fill, match highlighting, and even inline image support (via one of the extra modules). The Emacs 31 cycle brought nice improvements here too, including a fix for the scrolltobottom/fill-wrap dependency issue.
This is where Emacs Solo's completion story lives. Instead of
reaching for Vertico, Consult, or Helm, I use icomplete-vertical-mode,
which is built into Emacs. With the right settings it's surprisingly
capable:
(setq icomplete-delay-completions-threshold 0)
(setq icomplete-compute-delay 0)
(setq icomplete-show-matches-on-no-input t)
(setq icomplete-scroll t)
I've also been contributing patches upstream to improve icomplete's vertical rendering with prefix indicators. Some of those features are already landing in Emacs 31, which means the polyfill code I carry today will eventually become unnecessary.
A heavily customized Dired setup. Custom listing switches, human
readable sizes, integration with system openers (open on macOS,
xdg-open on Linux), and the dired-hide-details-hide-absolute-location
option from Emacs 31.
Writable Dired, so you can rename files by editing the buffer directly.
This one I'm particularly proud of. Emacs Solo's Eshell configuration includes:
— Shared history across all Eshell buffers: Every Eshell instance reads from and writes to a merged history, so you never lose a command just because you ran it in a different buffer
— Custom prompts: Multiple prompt styles you can toggle between
with C-c t (full vs. minimal) and C-c T (lighter vs. heavier
full prompt)
— A custom welcome banner with keybinding hints
— History size of 100,000 entries with deduplication
Enhanced incremental search with sensible defaults.
This is one of the largest sections and one I'm most invested in.
Emacs's built-in vc is an incredible piece of software that most
people overlook in favor of Magit. I'm not saying it replaces Magit
entirely, but with the right configuration it covers 95% of daily
Git operations:
— Git add/reset from vc-dir: S to stage, U to unstage,
directly in the vc-dir buffer. Admittedly, I almost never use this
because I'm now used to the Emacs-style VC workflow: C-x v D or C-x v =, then killing what I don’t want, splitting what isn’t ready yet,
and finishing with C-c C-c. Amending with C-c C-e is
awesome. Still useful once or twice a semester.
— Git reflog viewer: A custom emacs-solo/vc-git-reflog command
with ANSI color rendering and navigation keybindings
— Browse remote: C-x v B opens your repository on GitHub/GitLab
in a browser; with a prefix argument it jumps to the current
file and line
— Jump to current hunk: C-x v = opens the diff buffer scrolled
to the hunk containing your current line
— Switch between modified files: C-x C-g lets you
completing-read through all modified/untracked files in the
current repo
— Pull current branch: A dedicated command for git pull origin <current-branch>
— Emacs 31 settings: vc-auto-revert-mode,
vc-allow-rewriting-published-history,
vc-dir-hide-up-to-date-on-revert
Merge conflict resolution and diff viewing. Ediff configured to split windows sanely (side by side, not in a new frame).
Documentation at point, with eldoc-help-at-pt (Emacs 31) for
showing docs automatically.
The LSP client that ships with Emacs. Configured with:
— Auto-shutdown of unused servers
— No event buffer logging (for performance)
— Custom server programs, including rassumfrassum for multiplexing TypeScript + ESLint + Tailwind (I wrote a whole post about that)
— Keybindings under C-c l for code actions, rename, format, and
inlay hints
— Automatic enabling for all prog-mode buffers except
emacs-lisp-mode and lisp-mode
Diagnostics, spell checking, and whitespace visualization. All built-in, all configured.
The Emacs newsreader and email client. Configured for IMAP/SMTP usage.
Manual page viewer settings.
Fine-tuned minibuffer behavior, including completion-eager-update
from Emacs 31 for faster feedback during completion.
RSS/Atom feed reader built into Emacs. Customized with some extras I build my self for dealing with youtube feeds: thumbnail, transcripts, sending to AI for a quick summary, and so on.
Auto-closing brackets and parenthesis highlighting.
Process manager (like top, but inside Emacs).
Org-mode configuration, because of course.
File tree navigation in a side window. With Emacs 31, speedbar
gained speedbar-window support, so it can live inside your
existing frame instead of spawning a new one.
World clock with multiple time zones, sorted by ISO timestamp (Emacs 31).
Buffer name disambiguation when you have multiple files with the same name open.
Key discovery. Built into Emacs since version 30.
Quick web searches from the minibuffer. Configured with useful search engines.
Specific configurations for every language I work with, organized into three areas:
Common Lisp: inferior-lisp and lisp-mode with custom REPL
interaction, evaluation commands, and a poor man's SLIME/SLY setup
that actually works quite well for basic Common Lisp development.
Non-Tree-sitter: sass-mode for when tree-sitter grammars
aren't available.
Tree-sitter modes: ruby-ts-mode, js-ts-mode,
json-ts-mode, typescript-ts-mode, bash-ts-mode,
rust-ts-mode, toml-ts-mode, markdown-ts-mode (Emacs 31),
yaml-ts-mode, dockerfile-ts-mode, go-ts-mode. Each one
configured with tree-sitter grammar sources (which Emacs 31 is
starting to define internally, so those definitions will eventually
become unnecessary).
This is where the fun really is. Each of these is a complete,
standalone Elisp file that reimplements functionality you'd
normally get from an external package. They're all in lisp/ and
can be used independently.
I call them "hacky reimplementations" in the spirit of Emacs Solo: they're not trying to be feature-complete replacements for their MELPA counterparts. They're trying to be small, understandable, and good enough for daily use while keeping the config self-contained.
Custom color themes based on Modus. Provides several theme variants: Catppuccin Mocha, Crafters (the default), Matrix, and GITS. All built on top of Emacs's built-in Modus themes by overriding faces, so you get the accessibility and completeness of Modus with different aesthetics.
Custom mode-line format and configuration. A hand-crafted
mode-line that shows exactly what I want: buffer state indicators,
file name, major mode, Git branch, line/column, and nothing else.
No doom-modeline, no telephone-line, just format strings and
faces.
Enhanced navigation and window movement commands. Extra commands for moving between windows, resizing splits, and navigating buffers more efficiently.
Configurable format-on-save with a formatter registry. You
register formatters by file extension (e.g., prettier for .tsx,
black for .py), and the module automatically hooks into
after-save-hook to format the buffer. All controllable via a
defcustom, so you can toggle it on and off globally.
Frame transparency for GUI and terminal. Toggle transparency on your Emacs frame. Works on both graphical and terminal Emacs, using the appropriate mechanism for each.
Sync shell PATH into Emacs. The classic macOS problem: GUI Emacs
doesn't inherit your shell's PATH. This module solves it the same
way exec-path-from-shell does, but in about 20 lines instead of a
full package.
Rainbow coloring for matching delimiters. Colorizes nested parentheses, brackets, and braces in different colors so you can visually match nesting levels. Essential for any Lisp, and helpful everywhere else.
Interactive project finder and switcher. A completing-read
interface for finding and switching between projects, building on
Emacs's built-in project.el.
Vim-like keybindings and text objects for Viper. If you use
Emacs's built-in viper-mode (the Vim emulation layer), this
extends it with text objects and additional Vim-like commands. No
Evil needed.
Highlight TODO and similar keywords in comments. Makes TODO,
FIXME, HACK, NOTE, and similar keywords stand out in source
code comments with distinctive faces. A small thing that makes a
big difference.
Git diff gutter indicators in buffers. Shows added, modified,
and deleted line indicators in the margin, like diff-hl or
git-gutter. Pure Elisp, using vc-git under the hood.
Quick window switching with labels. When you have three or more
windows, this overlays single-character labels on each window so
you can jump to any one with a single keystroke. A minimal
reimplementation of the popular ace-window package.
Centered document layout mode. Centers your text in the window
with wide margins, like olivetti-mode. Great for prose writing,
Org documents, or any time you want a distraction-free centered
layout.
Upload text and files to 0x0.st. Select a region or a file and upload it to the 0x0.st paste service. The URL is copied to your kill ring. Quick and useful for sharing snippets.
Edit files as root via TRAMP. Reopen the current file with root
privileges using TRAMP's /sudo:: prefix. A reimplementation of the
sudo-edit package.
Multi-file regexp replace with diff preview. Perform a search-and-replace across multiple files and see the changes as a diff before applying them. This one turned out to be more useful than I expected.
Weather forecast from wttr.in. Fetches weather data from wttr.in and displays it in an Emacs buffer. Because checking the weather shouldn't require leaving Emacs.
Cryptocurrency and fiat exchange rate viewer. Query exchange rates and display them inside Emacs. For when you need to know how much a bitcoin is worth but refuse to open a browser tab.
Query cheat.sh for programming answers. Ask "how do I do X in
language Y?" and get an answer from cheat.sh
displayed right in Emacs. Like howdoi but simpler.
AI assistant integration (Ollama, Gemini, Claude). Send prompts
to AI models directly from Emacs. Supports multiple backends:
local Ollama, Google Gemini, and Anthropic Claude. The response
streams into a buffer. No gptel, no ellama, just
url-retrieve and some JSON parsing.
Git status indicators in Dired buffers. Shows Git status
(modified, added, untracked) next to file names in Dired, using
colored indicators in the margin. Think diff-hl-dired-mode but
self-contained.
Audio player for Dired using mpv. Mark audio files in Dired,
hit C-c m, and play them through mpv. You get a persistent mpv
session you can control from anywhere with C-c m. A mini music
player that lives inside your file manager.
File type icon definitions for Emacs Solo. The icon registry that maps file extensions and major modes to Unicode/Nerd Font icons. This is the foundation that the next three modules build on.
File type icons for Dired buffers. Displays file type icons next to file names in Dired. Uses Nerd Font glyphs.
File type icons for Eshell listings. Same as above but for
Eshell's ls output.
File type icons for ibuffer. And again for the buffer list.
Container management UI for Docker and Podman. A full
tabulated-list-mode interface for managing containers: list,
start, stop, restart, remove, inspect, view logs, open a shell.
Works with both Docker and Podman. This one started small and grew
into a genuinely useful tool.
M3U playlist viewer and online radio player. Open .m3u
playlist files, browse the entries, and play them with mpv. RET to
play, x to stop. Great for online radio streams.
System clipboard integration for terminals. Makes copy/paste work correctly between Emacs running in a terminal and the system clipboard. Solves the eternal terminal Emacs clipboard problem.
Eldoc documentation in a child frame. Shows eldoc
documentation in a floating child frame near point instead of the
echo area. A reimplementation of the eldoc-box package.
Khard contacts browser. Browse and search your khard address book from inside Emacs. Niche, but if you use khard for contact management, this is handy.
Flymake backend for ESLint. Runs ESLint as a Flymake checker for JavaScript/TypeScript files. Disabled by default now that LSP servers handle ESLint natively, but still available if you prefer the standalone approach.
Inline images in ERC chat buffers. When someone posts an image URL in IRC, this fetches and displays the image inline in the ERC buffer. A small luxury that makes IRC feel more modern.
YouTube search and playback with yt-dlp and mpv. Search YouTube from Emacs, browse results, and play videos (or just audio) through mpv. Because sometimes you need background music and YouTube is right there.
GitHub CLI interface with transient menu. A transient-based
menu for the gh CLI tool. Browse issues, pull requests, run
actions, all from a structured Emacs interface without memorizing
gh subcommands.
Throughout the config you'll see comments tagged ; EMACS-31
marking features that are coming (or already available on the
development branch). Some highlights:
— Window layout commands: window-layout-transpose,
window-layout-rotate-clockwise, and flip commands. Finally,
first-class support for rearranging window layouts
— Tree-sitter grammar sources defined in modes: No more
manually specifying treesit-language-source-alist entries for
every language
— markdown-ts-mode: Tree-sitter powered Markdown, built-in
— Icomplete improvements: In-buffer adjustment, prefix indicators, and better vertical rendering
— Speedbar in-frame: speedbar-window lets the speedbar live
inside your frame as a normal window
— VC enhancements: vc-dir-hide-up-to-date-on-revert,
vc-auto-revert-mode, vc-allow-rewriting-published-history
— ERC fixes: The scrolltobottom/fill-wrap dependency is finally resolved
— native-comp-async-on-battery-power: Don't waste battery
on native compilation
— kill-region-dwim: Smart kill-region behavior
— delete-pair-push-mark: Better delete-pair with mark
pushing
— World clock sorting: world-clock-sort-order for sensible
timezone display
I tag these not just for my own reference, but so that anyone reading the config can see exactly which parts will become cleaner or unnecessary as Emacs 31 stabilizes. Some of the polyfill code I carry today, particularly around icomplete, exists specifically because those features haven't landed in a stable release yet.
This latest cycle of working on Emacs Solo taught me a few things worth sharing.
Emacs gives you more than you think. Every time I set out to
"reimplement" something, I discovered that Emacs already had 70% of
it built in. vc is far more capable than most people realize.
icomplete-vertical-mode is genuinely good. tab-bar-mode is a
real workspace manager. proced is a real process manager. The gap
between "built-in Emacs" and "Emacs with 50 packages" is smaller
than the community often assumes.
Writing your own packages is the best way to learn Elisp. I
learned more about Emacs Lisp writing emacs-solo-gutter and
emacs-solo-container than I did in years of tweaking other
people's configs. When you have to implement something from
scratch, you're forced to understand overlays, process filters, tabulated-list-mode, transient, child frames,
and all the machinery that packages usually hide from you.
Small is beautiful. Most of the modules in lisp/ are under
200 lines. Some are under 50. They don't try to handle every edge
case. They handle my edge cases, and that's enough. If someone
else needs something different, the code is simple enough to fork
and modify.
Contributing upstream is worth it. Some of the things I built as workarounds (like the icomplete vertical prefix indicators) turned into upstream patches. When you're deep enough in a feature to build a workaround, you're deep enough to propose a fix.
Emacs Solo started as a personal challenge: can I have a productive, modern Emacs setup without installing a single external package?
The answer, after this cycle, is a definitive yes.
Is it for everyone? Absolutely not. If you're happy with Doom Emacs or Spacemacs or your own carefully curated package list, that's great. Those are excellent choices.
But if you're curious about what Emacs can do on its own, if
you want a config where you understand every line, if you want
something you can hand to someone and say "just drop this into
~/.emacs.d/ and it works", then maybe Emacs Solo is worth a
look.
The repository is here: https://github.com/LionyxML/emacs-solo
It's been a lot of fun. I learned more in this cycle than in any previous one. And if anyone out there finds even a single module or config snippet useful, I'd be happy.
That's the whole point, really. Sharing what works.
None of this exists in a vacuum, and I want to give proper thanks.
First and foremost, to the Emacs core team. The people who
maintain and develop GNU Emacs are doing extraordinary work, often
quietly, often thanklessly. Every built-in feature I configure in
init.el is the result of decades of careful engineering. The fact
that Emacs 31 keeps making things better in ways that matter
(tree-sitter integration, icomplete improvements, VC enhancements,
window layout commands) is a testament to how alive this project
is.
While working on Emacs Solo I also had the opportunity to contribute
directly to Emacs itself. I originally wrote markdown-ts-mode,
which was later improved and integrated with the help and review of
Emacs maintainers. I also contributed changes such as aligning
icomplete candidates with point in the buffer (similar to Corfu or
Company) and a few fixes to newsticker.
I'm very grateful for the help, reviews, patience, and guidance from people like Eli Zaretskii, Yuan Fu, Stéphane Marks, João Távora, and others on the mailing lists.
To the authors of every package that inspired a module in
lisp/. Even though Emacs Solo doesn't install external
packages, it is deeply influenced by them. diff-hl, ace-window,
olivetti, doom-modeline, exec-path-from-shell, eldoc-box,
rainbow-delimiters, sudo-edit, and many others showed me what
was possible and set the bar for what a good Emacs experience looks
like. Where specific credit is due, it's noted in the source code
itself.
A special thanks to David Wilson (daviwil) and the System Crafters community. David's streams and videos were foundational for me in understanding how to build an Emacs config from scratch, and the System Crafters community has been an incredibly welcoming and knowledgeable group of people. The "Crafters" theme variant in Emacs Solo exists as a direct nod to that influence.
To Protesilaos Stavrou (Prot), whose work on Modus themes, Denote, and his thoughtful writing about Emacs philosophy has shaped how I think about software defaults, accessibility, and keeping things simple. The fact that Emacs Solo's themes are built on top of Modus is no coincidence.
And to Gopar (goparism), whose Emacs content and enthusiasm for exploring Emacs from the ground up resonated deeply with the spirit of this project. It's encouraging to see others who believe in understanding the tools we use.
To everyone who I probably forgot to mention, who has opened issues, suggested features, or just tried Emacs Solo and told me about it: thank you. Open source is a conversation, and every bit of feedback makes the project better.
-1:-- Two Years of Emacs Solo: 35 Modules, Zero External Packages, and a Full Refactor (Post Rahul Juliato)--L0--C0--2026-03-08T12:00:00.000Z
I recently wrote about building major modes with Tree-sitter over on batsov.com, covering the mode author’s perspective. But what about the user’s perspective? If you’re using a Tree-sitter-powered major mode, how do you actually customize the highlighting?
This is another article in a recent streak inspired by my work on neocaml, clojure-ts-mode, and asciidoc-mode. Building three Tree-sitter modes across very different languages has given me a good feel for both sides of the font-lock equation – and I keep running into users who are puzzled by how different the new system is from the old regex-based world.
This post covers what changed, what you can control, and how to make Tree-sitter font-lock work exactly the way you want.
Traditional font-lock in Emacs actually has two phases. First, syntactic
fontification handles comments and strings using the buffer’s syntax table and
parse-partial-sexp (implemented in C) – this isn’t regexp-based at all.
Second, keyword fontification runs the regexps in font-lock-keywords against
the buffer text to highlight everything else: language keywords, types, function
names, and so on. When people talk about “regex font-lock,” they usually mean
this second phase, which is where most of the mode-specific highlighting lives
and where most of the customization happens.
If you wanted to customize it, you’d manipulate font-lock-keywords directly:
;; Add a custom highlighting rule in the old world
(font-lock-add-keywords 'emacs-lisp-mode
'(("\\<\\(FIXME\\|TODO\\)\\>" 1 'font-lock-warning-face prepend)))
The downsides are well-known: regexps can’t understand nesting, they break on multi-line constructs, and getting them right for a real programming language is a never-ending battle of edge cases.
Tree-sitter font-lock is fundamentally different. Instead of matching text with
regexps, it queries the syntax tree. A major mode defines
treesit-font-lock-settings – a list of Tree-sitter queries paired with faces.
Each query pattern matches node types in the parse tree, not text patterns.
This means highlighting is structurally correct by definition. A string is highlighted as a string because the parser identified it as a string node, not because a regexp happened to match quote characters. If the code has a syntax error, the parser still produces a (partial) tree, and highlighting degrades gracefully instead of going haywire.
There’s also a significant performance difference. With regex font-lock, every
regexp in font-lock-keywords runs against every line in the visible region on
each update – more rules means linearly more work, and a complex major mode can
easily have dozens of regexps. Poorly written patterns with nested quantifiers
can trigger catastrophic backtracking, causing visible hangs on certain inputs.
Multi-line font-lock (via font-lock-multiline or jit-lock-contextually)
makes things worse, requiring re-scanning of larger regions that’s both expensive
and fragile. Tree-sitter sidesteps all of this: after the initial parse, edits
only re-parse the changed portion of the syntax tree, and font-lock queries run
against the already-built tree rather than scanning raw text. The result is
highlighting that scales much better with buffer size and rule complexity.
The trade-off is that customization works differently. You can’t just add a regexp to a list anymore. But the new system offers its own kind of flexibility, and in many ways it’s more powerful.
Note: The Emacs manual covers Tree-sitter font-lock in the Parser-based Font Lock section. For the full picture of Tree-sitter integration in Emacs, see Parsing Program Source.
Every Tree-sitter major mode organizes its font-lock rules into features – named groups of related highlighting rules. Features are then arranged into 4 levels, from minimal to maximal. The Emacs manual recommends the following conventions for what goes into each level:
comment and definitionkeyword, string, typebracket, delimiter, operatorIn practice, many modes don’t follow these conventions precisely. Some put
number at level 2, others at level 3. Some include variable at level 1,
others at level 4. The inconsistency across modes means that setting
treesit-font-lock-level to the same number in different modes can give you
quite different results – which is one more reason you might want the
fine-grained control described in the next section.1
It’s also worth noting that the feature names themselves are not standardized.
There are many common ones you’ll see across modes – comment, string,
keyword, type, number, bracket, operator, definition, function,
variable, constant, builtin – but individual modes often define features
specific to their language. Clojure has quote, deref, and tagged-literals;
OCaml might have attribute; a markup language mode might have heading or
link. Different modes also vary in how granular they get: some expose a rich
set of features that let you fine-tune almost every aspect of highlighting, while
others are more spartan and stick to the basics.
The bottom line is that you’ll always have to check what your particular mode
offers. The easiest way is M-x describe-variable RET
treesit-font-lock-feature-list in a buffer using that mode – it shows all
features organized by level. You can also inspect the mode’s source directly by
looking at how it populates treesit-font-lock-settings (try M-x find-library
to jump to the mode’s source).
For example, clojure-ts-mode defines:
| Level | Features |
|---|---|
| 1 | comment, definition, variable |
| 2 | keyword, string, char, symbol, builtin, type |
| 3 | constant, number, quote, metadata, doc, regex |
| 4 | bracket, deref, function, tagged-literals |
And neocaml:
| Level | Features |
|---|---|
| 1 | comment, definition |
| 2 | keyword, string, number |
| 3 | attribute, builtin, constant, type |
| 4 | operator, bracket, delimiter, variable, function |
The default level is 3, which is a reasonable middle ground for most people. You can change it globally:
(setq treesit-font-lock-level 4) ;; maximum highlighting
Or per-mode via a hook:
(defun my-clojure-ts-font-lock ()
(setq-local treesit-font-lock-level 2)) ;; minimal: just keywords and strings
(add-hook 'clojure-ts-mode-hook #'my-clojure-ts-font-lock)
This is the equivalent of the old font-lock-maximum-decoration variable, but
more principled – features at each level are explicitly chosen by the mode
author rather than being an arbitrary “how much highlighting do you want?” dial.
Note: The Emacs manual describes this system in detail under Font Lock and Syntax.
Levels are a blunt instrument. What if you want operators and variables (level 4) but not brackets and delimiters (also level 4)? You can’t express that with a single number.
Enter treesit-font-lock-recompute-features. This function lets you explicitly
enable or disable individual features, regardless of level:
(defun my-neocaml-font-lock ()
(treesit-font-lock-recompute-features
'(comment definition keyword string number
attribute builtin constant type operator variable) ;; enable
'(bracket delimiter function))) ;; disable
(add-hook 'neocaml-base-mode-hook #'my-neocaml-font-lock)
You can also call it interactively with M-x treesit-font-lock-recompute-features
to experiment in the current buffer before committing to a configuration.
This used to be hard in the old regex world – you’d have to dig into
font-lock-keywords, figure out which entries corresponded to which syntactic
elements, and surgically remove them. Emacs 29 improved the situation with
font-lock-ignore, which lets you declaratively suppress specific font-lock
rules by mode, face, or regexp. Still, the Tree-sitter approach is arguably
cleaner: features are named groups designed for exactly this kind of
cherry-picking, rather than an escape hatch bolted on after the fact.
This part works the same as before – faces are faces. Tree-sitter modes use
the standard font-lock-*-face family, so your theme applies automatically.
If you want to tweak a specific face:
(custom-set-faces
'(font-lock-type-face ((t (:foreground "DarkSeaGreen4"))))
'(font-lock-property-use-face ((t (:foreground "DarkOrange3")))))
One thing to note: Tree-sitter modes use some of the newer faces introduced in
Emacs 29, like font-lock-operator-face, font-lock-bracket-face,
font-lock-number-face, font-lock-property-use-face, and
font-lock-escape-face. These didn’t exist in the old world (there was no
concept of “operator highlighting” in traditional font-lock), so older themes
may not define them. If your theme makes operators and variables look the same,
that’s why – the theme predates these faces.
This is where Tree-sitter font-lock really shines compared to the old system. Instead of writing regexps, you write Tree-sitter queries that match on the actual syntax tree.
Say you want to distinguish block-delimiting keywords (begin/end,
struct/sig) from control-flow keywords (if/then/else) in OCaml:
(defface my-block-keyword-face
'((t :inherit font-lock-keyword-face :weight bold))
"Face for block-delimiting keywords.")
(defun my-neocaml-block-keywords ()
(setq treesit-font-lock-settings
(append treesit-font-lock-settings
(treesit-font-lock-rules
:language (treesit-parser-language
(car (treesit-parser-list)))
:override t
:feature 'keyword
'(["begin" "end" "struct" "sig" "object"]
@my-block-keyword-face))))
(treesit-font-lock-recompute-features))
(add-hook 'neocaml-base-mode-hook #'my-neocaml-block-keywords)
The :override t is important – without it, the new rule won’t overwrite
faces already applied by the mode’s built-in rules. And the :feature keyword
assigns the rule to a feature group, so it respects the level/feature system.
Note: The full query syntax is documented in the Pattern Matching section of the Emacs manual – it covers node types, field names, predicates, wildcards, and more.
For comparison, here’s what you’d need in the old regex world to highlight a specific set of keywords with a different face:
;; Old world: fragile, doesn't understand syntax
(font-lock-add-keywords 'some-mode
'(("\\<\\(begin\\|end\\|struct\\|sig\\)\\>" . 'my-block-keyword-face)))
The regex version looks simpler, but it’ll match begin inside strings,
comments, and anywhere else the text appears. The Tree-sitter version only
matches actual keyword nodes in the syntax tree.
The killer feature for customization is M-x treesit-explore-mode. It opens a
live view of the syntax tree for the current buffer. As you move point, the
explorer highlights the corresponding node and shows its type, field name, and
position.
This is indispensable when writing custom font-lock rules. Want to know what
node type OCaml labels are? Put point on one, check the explorer: it’s
label_name. Want to highlight it? Write a query for (label_name). No more
guessing what regexp might work.
Another useful tool is M-x treesit-inspect-node-at-point, which shows
information about the node at point in the echo area without opening a separate
window.
Here’s a quick reference for the key differences:
| Aspect | Regex font-lock | Tree-sitter font-lock |
|---|---|---|
| Rules defined by | font-lock-keywords |
treesit-font-lock-settings |
| Matching mechanism | Regular expressions on text | Queries on syntax tree nodes |
| Granularity control | font-lock-maximum-decoration |
treesit-font-lock-level + features |
| Adding rules | font-lock-add-keywords |
Append to treesit-font-lock-settings |
| Removing rules | font-lock-remove-keywords |
treesit-font-lock-recompute-features |
| Suppressing rules | font-lock-ignore (Emacs 29+) |
Disable features via level or cherry-pick |
| Debugging | re-builder |
treesit-explore-mode |
| Handles nesting | Poorly | Correctly (by definition) |
| Multi-line constructs | Fragile | Works naturally |
| Performance | O(n) per regexp per line | Incremental, only re-parses changes |
The shift from regex to Tree-sitter font-lock is one of the bigger under-the-hood changes in modern Emacs. The customization model is different – you’re working with structured queries instead of text patterns – but once you internalize it, it’s arguably more intuitive. You say “highlight this kind of syntax node” instead of “highlight text that matches this pattern and hope it doesn’t match inside a string.”
The feature system with its levels, cherry-picking, and custom rules gives you
more control than the old font-lock-maximum-decoration ever did. And
treesit-explore-mode makes it easy to discover what’s available.
If you haven’t looked at your Tree-sitter mode’s font-lock features yet, try
M-x describe-variable RET treesit-font-lock-feature-list in a Tree-sitter
buffer. You might be surprised by how much you can tweak.
Writing this article has been more helpful than I expected – halfway through, I realized my own neocaml had function banished to level 4 and number promoted to level 2. Physician, heal thyself. ↩
-1:-- Customizing Font-Lock in the Age of Tree-sitter (Post Emacs Redux)--L0--C0--2026-03-08T08:30:00.000Z
The more I use gnosis, the more I notice design faults inherited from the software I was previously using.
Decks will be removed.
Similarly to Anki, decks provided a restriction, not a feature. A thema can only belong to one deck, many-to-one, while tags are many-to-many.
Themata in gnosis are already organized based on tags and you can
customize the review algorithm per tag using
gnosis-algorithm-custom-values.
Exports for “collections” will now be based on tags. I plan to add
tag filters for exports, e.g +anatomy -clinical to export all
themata tagged with anatomy, excluding clinical.
org-gnosiswill be merged intognosis, using a single unified database.
The idea was to have a minimal note taking system with a separate ui package that uses the browser, recreating the workflow I had with org-roam but with support for linking themata to nodes for topic-specific reviews.
In hindsight, this separation was a design mistake:
org-gnosis without gnosis has no real benefits.org-gnosis-ui that recreates an obsidian-style graph
in the browser provides no real benefit beyond “visualizing” the
collection.
tabulated-list in emacs to display nodes, their
backlinks/links and their linked themata provides actual workflow
usage. You can start reviews for specific nodes from the
dashboard, filter for contents and view relations, all within
emacs.Gnosis supports reviewing all themata linked to a specific node, which means themata are often reviewed before they are due. The interval calculation now uses elapsed time as the basis, but on success keeps the later of the computed/existing scheduled date. The thema’s score and review streak are still updated, so early reviews contribute to progression without distorting the schedule.
This ensures that early successful reviews cannot inflate or deflate intervals. On failure the computed interval is used directly since early failure is genuinely informative.
Gnosis exports will optionally include all the linked nodes for the exported themata. This means that current collections, like Unking, will provide ready-to-use collections where users can both review themata and browse the related material.
-1:-- Gnosis: Design Mistakes (Post Thanos Apollo)--L0--C0--2026-03-07T22:00:00.000Z
Rust doesn't have exceptions. Instead, functions and methods have to
return a special type to indicate failure. This is the Result type
which holds either the computed value if everything goes well, or an
error otherwise. Both, the value and error have types. So it's
definition is Result<T, E>, an enum that can be instantiated using
one of it's two variants Ok and Err.
1 2 3 | |
When a function returns a Result, it becomes mandatory for the caller
to handle it, otherwise it's impossible to extract the value it holds,
which is what the caller is interested in most of the time 1. The easiest
way to get the value out of a Result is to call it's unwrap
method, but there will be a panic in case of an error. It can be
thought of as an equivalent of an "unhandled" exception.
Calling unwrap is not necessarily a bad thing though. If there's
really no way to handle an error, it's better to let the process crash
than behave unpredictably. Arguably, "let it crash" is a valid
strategy to handle an unexpected error. But if your code is full of
unwrap calls (or it's close cousin expect), it's probably out of
laziness than intent.
A more respectable approach is to pattern-match the Result enum and
handle both cases.
1 2 3 4 5 6 7 8 | |
As a beginner, you may find pattern matching clearer and easier to understand. But soon it gets tedious to write and verbose to read. Much of the real world rust code written by experienced devs will use convenience features that rust provides to make error handling less tedious, which is essentially the topic of this post, but we will get to that in a bit.
Like me, if you're coming to rust from a dynamic languages background, the first issue you'll probably run into is figuring out how to return different types of errors from the same function. For example, suppose you're writing a function to read a file and parse it's contents, you may have to propagate two types of errors to the caller:
std::io::Error in case of failure to read the fileu64 for the sake of this
example)As a beginner, you may be tempted to convert both errors into String
and use it as the error type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
Now any one who has written any serious software would most certainly get a bad feeling about this. There's often a need to be able to classify errors eventually, and doing that with strings is a terrible idea, especially in a typed language. A good example of the need to classify errors is a typical web app. Based on the reason for failure, you want to respond with an appropriate HTTP status code: A validation error must result in a 400 while failure to connect to the database must be a 5xx. Imagine having to match strings againstg regular expressions in order to take this decision.
I first ran into this problem in mid 2023, when LLMs had yet to gain popularity as search engines. So I googled the old-fashion way and landed on this gem of a post - https://burntsushi.net/rust-error-handling/. In particular, defining a single enum that can wrap over multiple error types was an eye opener for me.
Here's the same code with a custom defined error type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
I've intentionally written it in a tedious way, by pattern-matching every single result value so that you can clearly see what's going on. It doesn't have to be this verbose. There are two features of rust that we can use to elegantly trim down this code.
map_err function combinator helps convert error, if any, from
one type to another and,?) helps with error propagation1 2 3 4 5 6 7 8 9 10 11 12 | |
The use of enums for wrapping multiple error types into a single error
type and then converting specific errors to it using map_err was an
aha moment for me. IIRC, I may not have read the rest of that post
by burntsushi with as much attention afterwards. Having found a
workable solution, I stuck to it for quite some time without realizing
that I was missing out by not using the convenience features rust
provides for dealing with the Result and Error types more
elegantly.
And that's what this post is about. Thank you for reading this far but here is where this post actually begins! If you're just starting with rust or still getting used to it, hope you will find it useful.
Before picking up rust, I wrote Clojure professionally for 9 years and
dabbled in several flavours of lisp such as scheme, racket, emacs
lisp. Naturally I was quite happy to learn about iterators and the
familiar functional abstractions they provide - map, filter,
fold etc. But, soon I realized that error propagation from inside
the fn closure passed to
Iterator::map
is directly not possible as it expects a closure that returns T and
not Result<T, E>. For example, the following doesn't compile:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
1 2 3 4 5 6 7 | |
This one compiles but an error gets collected in the result and not propagated immediately when it's encountered. Often that's not what we want.
1 2 3 4 5 6 7 8 | |
1 2 3 4 5 | |
So I gave up and preferred using simple for-loops in such cases,
1 2 3 4 5 6 7 8 9 | |
Turns out, there's a way to stop iteration upon encountering an error and propagate it up the call stack.
1 2 3 4 5 6 7 | |
This may feel like magic at first but once you understand the
FromIterator trait, it makes perfect sense. This trait is already
implemented for the Result type. The
doc
explains it quite nicely.
Takes each element in the Iterator: if it is an Err, no further elements are taken, and the Err is returned. Should no Err occur, a container with the values of each Result is returned.
So the call to collect returns exactly what's specified as the type
declaration - Result<Vec<u8>, AppError> and the question mark
operator can be used immediately to extract the value or propagate the
error.
Similarly, when using the .map method on an Option type, it's not
possible to propagate the result directly from inside the closure.

1 2 3 4 5 6 | |
Sure, explicit pattern-matching works:
1 2 3 4 5 6 7 8 9 | |
But with Option values, you may end up repeating such code many times. A
more concise way is to have the closure return a Result, so you'd
get Option<Result<T, E>> which can then be converted to
Result<Option<T>, E> by calling the transpose method.
1 2 3 4 5 6 7 | |
Here I'm using multiple steps with explicit type declarations for
clarity but the same can be expressed as a one-liner too -
x.map(process).transpose()?. An experienced rust programmer should
easily be able to recognize this pattern and understand what's going
on.
My general observation is that anytime one of the arms of a match
block is None => None or Err(e) => Err(e), there must be a more
concise and elegant way to write it.
Initially it wasn't clear to me why the Error trait was important. I
never had to define it for any of my custom error types. Like any
other trait, the rust compiler wouldn't complain if an error type
doesn't implement it. It's only when a certain part of code requires
it through trait bounds, that you need to be implemented for the code
to compile. It's more of a convention that strictly enforced
requirement.
I didn't care about the Error trait for many months, until I
attended a deep dive session about the anyhow crate at the Rust
Bangalore meetup where the speaker
Dhruvin Gandhi explained it in great detail.
Good news is that the Error trait provides all the methods as long
as Debug and Display traits are implemented, so all you need to
implement is these two traits.
Let's modify the AppError so that it implements the Error trait
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
As you can see, in case of the Io and ParseInt variants we could
assume that the underlying error types would have the Display trait
implemented, so we could directly use it for string interpolation.
Similarly, we could directly annotate AppError with
#[derive(Debug)] only because std::io::Error and
std::num::ParseIntError implement Debug.
Finally, the impl block for the Error trait remains empty because, as mentioned above, all the trait methods have default implementation already provided.
Essentially, implementing the Error trait makes it easy for multiple
error types to work well with each other. You'll see a concrete
example towards the end of this post.
Software is written in layers. Often, error types defined in high
level code have variants wrapping over the low level error
types. We've already come across this in the AppError example
above - The AppError::Io variant is a wrapper for the low level
std::io::Error type.
In such cases, you'll often notice the same .map_err expression
repeated multiple times in high level functions. Here's an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
The repetition can be avoided by implementing the From trait for the
higher level error
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
As the return type of the function is known, the ? operator takes
care of converting to it by calling the corresponding From
implementation.
That brings us to the thiserror
crate. When I was first searching how to return two types of errors
from a single function, I came across many resources that suggested
the thiserror and anyhow crates. As a beginner I felt a bit
overwhelmed by both at that time. Specially since the custom enum
approach worked and seemed elegant enough, why bother including
additional dependencies?
But once I started implementing Error trait for my custom error
types, thiserror began to make sense.
With thiserror the above AppError definition along with the
Display and From implementations can be compressed into:
1 2 3 4 5 6 7 8 9 10 11 | |
As you can see, the Display trait is generated from the annotations
with "inline" error messages. And the From trait implementation is
generated for variants that have the #[from] attribute. This is
incredibly convenient. Implementing Error traits for all error types
doesn't feel like a chore any more. I use thiserror in all my
projects.
I am still not convinced about anyhow though. It feels a bit too
much convenience for my comfort, where you stop caring about the
individual error types altogether. I have never used it in a serious
project, so I could be wrong.
If you are implementing a library or a crate that others use in their code, there are a few things you might want to take care of. These are also applicable to error types exposed by low level code and not just external dependencies.
Always implement the Error trait for the error types that are
publicly exposed by the crate. Had std::io::Error not implemented
the Error trait, it wouldn't have been possible to use thiserror for
the AppError enum.
In most cases, a low level error type gets converted to a higher level error type as we've seen in previous examples in this post. But sometimes it's possible that a high level error type (defined in an app for example) has to be converted to a low level type (defined in a library). This happens especially when the library exposes a trait requiring a method that returns a low level error type.
I encountered this when implementing
plectrum - a crate that helps
with mapping lookup/reference tables in a database with rust enums. It
requires the user to implement a DataSource trait, the definition of
which is:
1 2 3 4 5 6 | |
Notice the return type of the load method has plectrum::Error
type. In the initial version of the crate, I had provided a
Sqlx(sqlx::Error) variant in the plectrum::Error enum as sqlx is
my preferred SQL library. But what about those who use other libraries
or ORMs?
In a later version, I added a generic DataSource variant2:
1 2 3 4 5 6 | |
Plectrum users can now wrap any error type in
plectrum::Error::DataSource variant as long as it implements the
Error trait as well as the Send and Sync marker traits.
This is a good example of why implementing the Error trait is
important for your custom error types.
1. Sometimes the caller doesn't really care
about the returned value, but if the result is not "used", which
essentially means handled, the compiler shows a warning. That's
because the Result enum is annotated with the #[must_use]
attribute. Refer Result must be
used. ↩
2. Note that here we're annotating the inner
type with #[source] instead of #[from] that we saw earlier. This
is because Box<dyn Error> is a generic catch-all, not a specific
type we'd want auto-converted from. ↩
-1:-- Handling Rust errors elegantly (Post Vineet Naik)--L0--C0--2026-03-07T18:30:00.000Z
[This post, aimed at text editing aficionados especially in the Emacs and Vim camps, advocates using Emacs Evil in a configuration that replaces Evil Insert state with vanilla Emacs to gain the best of both Emacs and Vim editing styles. It also describes the very handy evil-execute-in-normal-state and its use from a vanilla Emacs editing state, to make the experience even more syncretic and seamless.]
Typos are incredibly common. How do you fix them?
For example, a common one I’ve noticed is in typing the name of an identifier in a programming language, like so:
custom0print|
… when I meant to type
custom-print|
In Emacs, the only way I know of to fix this is to C-b repeatedly to get to the 0 and then C-d followed by - to replace it, and then return to the end using M-f or C-e [A commenter on Reddit suggested using C-r! — a much better option].
In Vim/Evil, you’d first Escape to Normal mode. Then you’d type F0 to find the 0 looking back from the cursor, and then r- to replace it with a dash. Then you’d return to editing at the end using something like ea or simply A.
I don’t love either of these approaches.
Emacs is tedious here [although C-r is great! And it makes the point this post is making, regarding the benefits of combining Emacs and Vim, even more definitively], but it is at least simple and thus keeps you focused on the subject matter. And while Vim/Evil is elegant, escaping to Normal state to perform this minute edit and then returning to where you were before loses “tempo,” causing you briefly to focus on something else than your subject matter. It is a cost that is especially felt for minute edits (for larger edits, escaping to Normal state is perfectly fine as the edit does require your attention).
The good news is, we can combine them for the best of both worlds.
Here’s how I do it:
;; Use Emacs keybindings when in Insert mode }:)
(setcdr evil-insert-state-map nil)
As vanilla Emacs is designed to be a standalone editing paradigm, I find it generally more useful and powerful than Evil Insert state.
evil-execute-in-normal-state to a convenient key (I use C-;. By default, Evil Insert state uses C-o, but Emacs’s C-o is very useful, so it’s better to override C-; which is perhaps even easier to type as it’s in home position). (define-key
;; Insert mode's "C-o" --- roundtrip to Normal mode
evil-insert-state-map
(kbd "C-;")
#'evil-execute-in-normal-state)
Now you can use C-; to enter a Normal command which of course in this case is F0. You remain in Emacs state, allowing you to C-d - and then return with M-f or C-e or whatever fits the specific case, without leaving the insertion state, keeping you focused on the task at hand and preserving your flow. This approach thus gains the efficiency of the Evil solution while still feeling light.

The C-; you’ve now bound is very handy, and this is just one example of its use. It is useful in any situation where you want to do a quick edit somewhere else without losing tempo. In such cases, Vim allows you to describe your edit in a natural editing language, while Emacs keeps you in the flow. You’d like to momentarily use the description language but otherwise keep doing what you’re already doing. As it happens, even aside from tempo considerations, in many cases using this approach is more efficient than either Emacs or Evil on their own.
Now, if you’re a purist Vim or Evil user, don’t worry! This isn’t blasphemy but is a feature that’s part of Vim itself! Evil, like Vim, bounds your edits as you would expect, so that it is functionally identical to explicitly escaping, editing, and re-entering Insert state.
My Vim tip on Living the High Life elaborates on this.
Do you use Emacs and Evil together for the best of both? What are your favorite tricks?
-1:-- On “Tempo” in Text Editing (Post Sid Kasivajhula)--L0--C0--2026-03-07T18:29:04.000Z
I am developing four new themes for my doric-themes package:
doric-almond (light)doric-coral (light)doric-magma (dark)doric-walnut (dark)Each of them has its own character, while they all retain the minimalist Doric style. Below are some screenshots. Remember that these themes use few colours, relying on typography to establish a visual rhythm.
All four themes are in development. I may still make some minor
refinements to them, though I have already defined their overall
appearance. If you like the minimalism of the doric-themes, I think
you will appreciate these new additions.
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: four new themes are coming to the ‘doric-themes’ (Post Protesilaos Stavrou)--L0--C0--2026-03-07T00:00:00.000Z
So emacs plus (through homebrew on macOS) keeps giving me this error: Invalid function: org-element-with-disabled-cache.
Does anyone know what this is about, and why it’s happening? No issue with Emacs on Linux (same config) or when I had emacsformacos (same config)
I believe I fixed it this morning (3/11) by removing Emacs-plus completely and reinstalling.
brew uninstall emacs-plus@30brew cleanup (this removes dependencies, where I think the issue was)brew install emacs-app (which is now emacs plus, from what I got through Homebrew)I also ran Brew Doctor between steps 2 and 3 and found a couple of issues I resolved, which shouldn’t be related, but you never know.
What started this whole thing, I think, was that I wanted to try the new org-mode on top of the old org-mode. I am not too sure, but it seems like that was the problem.
-1:-- (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-03-06T19:36:36.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!