Irreal: Automatically Signing And Encrypting Email With Mu4e

Nicolas Cavigneaux has an excellent post on signing and optionally encrypting emails sent with mu4e. The idea is to sign each email you send and if you have a GPG key for every recipient, encrypt it was well. As Cavigneaux says, it’s pretty easy to sign and/or encrypt messages with mu4e but you have to remember to do it and most people, including him, don’t always remember.

The solution, of course, is to automate the process so that you don’t have to remember. That turns out to be pretty easy to do. Cavigneaux has the necessary code in his post. The basic flow is to check if you have everyone’s encryption key and, if so, encrypt the message. Regardless of whether or not everyone’s encryption key is available sign the message. There are calls available for both the signing and encryption. The process is kicked off with the send-message-hook every time you send a message.

There was a time when encrypting all your messages was the thing to do but difficulties with key distribution have pretty much put an end to the encrypt all emails movement. Most people don’t have keys and those who do will probably be annoyed to receive mundane emails that are encrypted. For those sensitive emails being sent to people you know have keys, you pretty much automatically remember to encrypt them.

That leaves signing. It mostly doesn’t hurt anything1 so it probably doesn’t matter if you always sign your messages, especially if you have reasons to suspect that their contents might come into dispute. I’ve stopped doing even that because, as I say, no one has any idea what it means.

So my takeaway is that if you’re paranoid about certifying the content of your emails or you regularly deal with people who want to ensure themselves that they’re communication with you, it may make sense to automatically sign your emails. Except in special circumstances, I don’t see any reason to encrypt all your emails.

Footnotes:

1

Although I do remember being queried about those unreadable binary blobs at the end of my emails.

-1:-- Automatically Signing And Encrypting Email With Mu4e (Post Irreal)--L0--C0--2026-07-03T15:06:33.000Z

Sacha Chua: Re: React to Sacha and Prot Newbies and Starter Kits Emacs Video - linkarzu

: All right, quick video form of this post is at Yay Emacs 35: Reacting to Linkarzu's reaction to my video with Prot about newbies and starter kits - YouTube in case anyone wants. That's how it works, right? =)

Hey hey hey, now I'm linkarzu-famous. =) Linkarzu (Christian Arzu) posted a reaction video to the first part of YE24: Sacha and Prot Talk Emacs - Newbies/Starter Kits. Here's his vid:

YouTube might be holding my comment for moderation because I tried to add too many links to it. I also realized my timestamps were off in my YT comment, so here it is along with other stuff I've just added.

On my Newbies/Starter Kits chat with Prot

It's definitely more of a meta-discussion (how can we make the newcomer experience better?) than something directly focused on helping newbies, but I hope you're getting something out of it. Think of it like a live coaching session for me so that I can figure out what to prioritize on my TODO list to make the newcomer experience better, with some ideas and questions thrown out there in case other people want to work on things too.

Learning Emacs in order to organize your life

02:20:13 What do I want to do with Emacs? I think, like I've said it before, organize my life a little bit better. Try Org, pretty much. I don't care about Emacs for editing Markdown files. That wouldn't make sense, you know, because I can do that in Neovim quite well. And I don't want to replicate that in Emacs. That's just going to be a waste of time. Something that I don't do in Neovim. This is something that the professor said as well: Just use Emacs for something that you don't do in Neovim right now.

If you want to learn Emacs so you can use it to organize your life, there are more direct paths than my video about newbies/starter kits. You might be just fine with the basic tutorial (Ctrl+h t within Emacs), the Org Mode compact guide, maybe another Org Mode tutorial that matches the way you think, and some time experimenting with the basics until you figure out the kinds of things you'd like to improve. The idea is to quickly get to the point where this is useful, and then start using some of the time/energy saved to learn more. A simple progression might start with something like this:

  • Opening: Use Emacs to open and close your todo.org. No keyboard shortcuts needed, just open the file, type, and use the toolbar or menu bar to save. If you're in a console Emacs and you don't want to use the mouse, you can use F10 to open the menu.
  • Leaving Emacs open: Realize you can save time by just leaving Emacs open with the file instead of opening/closing it all the time. This is probably more of a mindset change for Vim users. Set up your window manager so that you can switch to Emacs with a convenient keyboard shortcut. I use super+1.
  • Themes? If the default theme gets on your nerves, figure out how to change it. M-x customize-themes is a good starting point. If you're not sure what that means, go through the tutorial (Help - Emacs Tutorial).
  • Keyboard shortcuts: Get annoyed with using the toolbar or menu bar to save. Get the hang of C-x C-s (save-buffer). Start to get your mind used to the idea of keyboard shortcuts being different in different apps. Try not to give in to the temptation to make this C-s like in other apps. C-s is isearch-forward, which you will probably eventually find really useful, and if you move that you will end up needing to move whatever you are moving it to. Use sticky notes to remind yourself of the handful of keyboard shortcuts you're learning.
  • Basic Org Mode tutorial: Read an Org Mode tutorial, maybe this one. Start with * TODO ... headings. You can manually type them. Change TODO to DONE. Again, this can be pretty manual.
  • Org markup: Learn how to open links, make subheadings (**, ***), etc.
  • Shift: Get annoyed with manually typing TODO keywords. Experiment how to use shift left and shift right. (Might not work on console Emacs, depending on what keyboard shortcuts your terminal supports.)

Feel free to switch steps around depending on where the friction is. Depending on what you want to do from here, you might want to learn about scheduling things and displaying an agenda or setting up capture (which gets even more useful as you do more things within Emacs, since it can automatically pick up links to whatever you're looking at).

Also, along the way, it could be worth flipping through the StarterKits page on the EmacsWiki to see if one of those options matches the way you think (totally optional), or maybe chat with Prot so he can help translate what you want into the keywords you can use to find stuff or the priority to learn things in. Meetups are great too.

Learning Emacs with people

  • 00:33:20 I need a daddy that holds my hand and guides me through the process. I'm lost. And chat is even way looser. Even more lost, you know, because they're like, try Doom, try Evil and try this and try the other one, you know, and run Neovim inside Kitty and no, run Emacs inside Kitty or no, use the GUI. No. So it's pretty confusing.
  • 01:28:45 You know what we should do, okay? We should pay Prot for his coaching sessions. He's in Greece, right? He's in Cyprus. To give something back to him. But I don't know if he's okay in transmitting this live because all of these cheap ass MFers watching that would be watching this live stream will not pay Prot. So that's bad business, brother, because the live streams are just going to stay there. But if I do it for myself, if I pay to him, we have the one-on-ones and I don't post them and I learn Emacs, you know, that is not… That's going to leave you guys behind.

Yup, mentorship/coaching is totally a great way to learn Emacs. Prot is okay with people livestreaming or posting a recording of the coaching session. This livestream is actually one of those instances - I set it up as a coaching session with him! =) Amin Bandali has also posted some of his sessions with Prot (FFS code review and Emacs extensibility with Protesilaos, FFS code review with Protesilaos - bandali). I love your recent livestreams about exploring Emacs. Learning out loud is fantastic. It lets other people help out, and you help lots of people along the way. If you're comfortable with the idea, I think livestreaming or posting a recording of a coaching session with Prot would be wonderful. You've mentioned wanting to use Emacs to organize your life, so it's of course totally okay to chat privately. That way you don't have to worry about leaking any private information. Either way works!

This thing about balancing learning from resources and learning from people is an interesting one to think about. On one hand, we don't want a flood of generic requests from help from people who haven't bothered to look things up for themselves. On the other hand, because Emacs is so large and so many things are possible (and also oddly-named), it really helps to be able to talk to people. It's like the way you could learn how to play the piano or speak a different language by yourself, but a piano teacher could help you pick the right pieces for your level, and a tutor can help you with the nuances and pronunciation feedback that a dictionary or a textbook can't. I think learning how to learn from both resources and people is definitely a good skill worth working on during the early days, which could include:

  • taking notes and sharing them - great way to solidify your knowledge and pay it forward
  • learning how to skim tutorials and references to pick up ideas and terminology without feeling like you're progressing too slowly
  • learning how to break the things you want into bite-sized chunks so that they can actually fit into your brain; use sticky notes and text files to help you
  • connecting with people, learning how to ask questions

Reddit links

00:35:32 I'm not an Emacs user. I'm an Emacs, I don't know, tester or trier, whatever. If I come to the Emacs subreddit, for me, it's just a waste of time because I will not be able to find anything. I don't know where to find stuff. Maybe if I go to the about page, but who does that? Okay. Who in their sane mind comes here to the about page?

… Getting started, maybe here. Get Emacs. Emacs resources. Okay, but if I go to the EmacsWiki… How to edit Elisp area. Like there's a thousand links here and that is just the first link, brother. Then I have this other one. WikEmacs. OK, I trust it, so I'm just going to hit continue. WikEmacs.org. That one doesn't even load. Emacs reference. Not found. OK, so you see what I'm saying? This one has a thousand links and I just by looking at the amount of them, I'm like, nope, don't want to look at that. Maybe this one. The book, OK, the book, this is the book that Aaron recommends.

… So I think the suggestion that Prot is sharing there is quite useful, to be honest. Have them pinned here. I have something like that in my subreddit, you know, things that I want people to see when they get there. So when they get to the server, the first thing that I want them to see is this: The Discord information that the podcast has been moved to a different YouTube channel. This is useful as well. … So yeah, this is actual information that I want people to know. If those would be updated on the Emacs subreddit, I think it would be a good idea.

Oooh, good catch. I've messaged the r/emacs mods about the dead links, suggested direct links to Emacs Newbie and Starter Kits on the EmacsWiki, and suggested adding Doom Emacs and Spacemacs if Better Defaults is listed. The sidebar isn't very visible (most people miss it, especially on mobile), but every little bit helps. Usually what happens is a newbie posts about their question on r/emacs and someone replies with some helpful resources. An Automod or a sticky might help. I sent your video + timestamp to the r/emacs mods.

Timestamps

  • 44:01 So how does she add those timestamps? … Oh, she typed some magic there. She typed something and then a timestamp was added.
  • 01:30:25 We could see what she typed there. Let's see. Is she in normal mode? Does she use modal navigation or something? Let's see. OT. She typed OT and then she… OT and then she presses a key, which probably expands the snippet or something.

I have an abbreviation "ot" that expands to the timestamp after I press space, comma, other punctuation, tab, whatever. This is convenient for me to type because it's home-row on Dvorak. Here's the relevant part of my config:

(setq-default abbrev-mode 1)
(define-abbrev global-abbrev-table "ot" ""
  (lambda () (insert (format-time-string "[%Y-%m-%d %a %H:%M]"))))

Big picture: I added this abbreviation for timestamps because I wanted a quick way to keep track of highlights, things to clip, possible chapter markers, etc. I could calculate it as a relative time using org-timer (there's a built-in feature), but wall-clock time is easier to use in calculations in case I want to adjust it later on. So, for example, I now have a little bit of code (sacha-stream-org-convert-timestamps-to-youtube-offsets in my config) that replaces all the timestamps in a selected region with the offsets based on the start time of the livestream that includes those timestamps. I can export the selection into a plain-text format that I can paste into the YouTube video description for quick chapter markers. Then I can bulk-add comments with those timestamps into the VTT transcription produced by WhisperX (subed-vtt-insert-chapter-comments in subed), move them earlier or later to match the actual times, copy the corrected chapter markers into YouTube (subed-section-comments-as-chapters), and use those chapter markers when publishing the transcript (using Org Mode and a custom link type). This is because I don't usually have the patience to listen to my whole video again and I don't expect people to have the patience to listen to my whole video either, so I want people to be able to quickly jump to the parts that might be interesting for them. =) I'm not sure this is a workflow you can easily pick up if you're starting from scratch (… haven't confirmed that it actually works for anyone other than me…), but I'm mentioning it to give kind of the big picture of why I have that snippet and what else it enables. Because Emacs!

Timestamps are very handy. I even have some code that schedules a YouTube livestream for the Org timestamp at point (sacha-stream-org-schedule-livestream-for-entry-at-point), using the title and body of the Org subtree and uploading the thumbnail from the Org entry :THUMBNAIL: property (or a default property). It inserts the YT embed. I have another function for setting up a Google Calendar entry so I can invite the guest (sacha-emacs-chat-schedule). I mess up times and timezones all the time, so the less I have to manually click on stuff, the better.

My evil plan

1:31:36 Now why is she interested in doing all this, brother? This is a pretty good person, actually. Why is she so concerned about the experience for newcomers in Emacs? On the Neovim side of things, it's like, brother, you're just on your own. "F* yourself, go and watch some videos, and if you get it, awesome." Now, there's really amazing people as well on the Neovim side of things. I'm just talking shit, but I'm honestly curious, like… She's really concerned about new people joining into the Emacs church. Is this a church really? Like, okay, do we have to pay after once we're part of the church? Like, do we need to give like 10% of our income to the Emacs church?

Hah, it's all part of my Evil Plan. (Not to be confused with evil-mode.) Sure, Emacs isn't a good fit for everyone. I think the people who seem to really click with it and with other people who use it are the ones who enjoy tinkering and who can (mostly) find the balance between getting stuff done and tweaking their setup. =) If this might be your jam, I hope you can get past the initial hump and get to the point where it gets to be fun and useful! Sometimes it takes several tries for it to stick. We have lots of stories of people who didn't get Emacs the first time around, but who eventually figured it out later. I love that there are so many people who've used Emacs to make a TODO system that actually works for them. (It's usually Org Mode, but sometimes it's something else, that's all cool.) I love that people can do little tweaks to remove friction or make new things possible step by step.

So, the evil plan:

  1. If people learning Emacs can connect with resources and people who can help them enjoy figuring things out, then…
  2. they'll get to the point where they can come up with ideas and make things better for themselves.
  3. This often turns out to be useful for other people too,
  4. and then people can bounce ideas around and make things even better.
  5. So, years down the line, I'll want to do something crazy with Emacs and someone will already have written a function for doing it. ;)

See? I'm just planning ahead. Bwahaha! Also, I love seeing the kinds of cool things people come up with and share, even if I might not personally need it (yet). It's fun. I hope you get the hang of it. I think that could lead to lots of interesting conversations. Even if you decide to use something else, that's cool too. The important thing is that you're figuring out stuff that works for you! =)

View Org source for this post

You can e-mail me at sacha@sachachua.com.

-1:-- Re: React to Sacha and Prot Newbies and Starter Kits Emacs Video - linkarzu (Post Sacha Chua)--L0--C0--2026-07-03T12:24:23.000Z

Emacs Redux: Automating Emacs Screenshots

I maintain a bunch of theme ports these days: Zenburn, Solarized, Tokyo Night, and most recently Batppuccin. Every one of them wants screenshots in the README, ideally one per variant, so people can compare the flavors at a glance.

I’d been putting this off forever, because taking the screenshots by hand is such a chore. Load a theme, resize the frame, arrange a nice-looking buffer, take a screenshot, crop it, then repeat for every single flavor. And the results are never quite consistent: the font is a little different, the window is a slightly different size, the crop is off by a few pixels. Multiply that by four flavors across several themes and you can see why I kept finding better things to do.

So recently I finally did what I should have done from the start and taught Emacs to take the screenshots for me. What started as a five-minute hack turned into a surprisingly deep little rabbit hole, so I figured it was worth a write-up.

The core idea is simple: spin up a throwaway emacs -Q, load the theme, show a sample buffer in a frame of a fixed size, and capture just that window. Because every step is scripted, the screenshots come out identical in layout, and regenerating the whole set after a color tweak is a single command.

Four Batppuccin flavors, generated automatically

A clean, disposable frame

The first half of the job is pure Emacs Lisp. I want a frame with no distractions (no tool bar, menu bar, or scroll bars), a nice font, a fixed size, and of course the theme loaded:

(setq inhibit-startup-screen t)
(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(setq-default cursor-type nil)          ; hide the cursor for a clean shot
(set-face-attribute 'default nil :family "Fira Code" :height 150)

(add-to-list 'custom-theme-load-path "/path/to/theme")
(load-theme 'batppuccin-mocha t)

(set-frame-size (selected-frame) 92 36)
(find-file "sample.el")

I load all of this into a throwaway Emacs:

emacs -Q --eval '(load "setup.el")'

A couple of things bit me here that are worth calling out:

  • emacs -L some/dir puts a directory on the load-path, but load-theme searches custom-theme-load-path. Those are two different lists, so remember to add the theme’s directory to the latter.
  • If your sample file lives in a project with a .dir-locals.el, opening it can pop up an “unsafe local variables” prompt that blocks the whole script. Setting enable-local-variables and enable-dir-local-variables to nil sidesteps that.

I use an Emacs Lisp file as the sample, by the way. It highlights nicely and, being the mother tongue, needs no third-party major mode, which keeps the whole setup dependency-free.

Now there’s a pretty Emacs frame on screen. The other half of the problem is turning it into a PNG.

Option 1: let Emacs export itself

The cleanest approach doesn’t involve a screenshot at all. If your Emacs is built with Cairo (as the GTK build on Linux typically is), it has a wonderful function called x-export-frames that renders a frame straight to an image:

(with-temp-file "shot.svg"
  (insert (x-export-frames nil 'svg)))

It can emit svg, pdf, postscript, or png. No external tools, no window-manager wrangling, no “don’t touch the mouse while it runs”. Emacs simply hands you the picture. If you’re on a Cairo build, stop reading and use this.

Alas, I’m on macOS, where Emacs uses the NS toolkit and x-export-frames isn’t available (you get a friendly void-function). So I had to go the screenshot route.

Option 2: screenshot the window

macOS ships with screencapture, which can grab a rectangular region of the screen. The trick is knowing exactly where the Emacs frame is. Conveniently, Emacs knows: frame-edges reports the outer pixel coordinates of the frame:

(let ((e (frame-edges nil 'outer-edges)))
  (with-temp-file "/tmp/geom"
    (insert (format "%d %d %d %d" (nth 0 e) (nth 1 e) (nth 2 e) (nth 3 e)))))

The shell side reads those coordinates and captures the rectangle:

read L T R B < /tmp/geom
screencapture -x -R "$L,$T,$((R - L)),$((B - T))" out.png

And that’s basically it. Except it isn’t.

The part nobody warns you about

Here’s the catch with region capture: screencapture -R grabs whatever happens to be on screen at those coordinates. If any other window is sitting on top of the Emacs frame, you’ll cheerfully capture that instead. And since I like to keep working while the script churns through a dozen flavors, this happened all the time. I’d end up with a screenshot of my terminal, my browser, or my other Emacs.

I ended up stacking a few tricks. First I float the frame above everything else with the z-group frame parameter, (set-frame-parameter nil 'z-group 'above). Then, since an Emacs launched from a background shell isn’t the active application and starts out buried behind the other windows, I pull it forward with (ns-hide-emacs 'activate).

Neither of those is bulletproof on its own, so the real safety net is checking the result. After each capture I sample a pixel from a corner that should be the theme’s background and compare it to the color Emacs reports for the default face. If they don’t match, I know I grabbed the wrong window, so I wait a moment and try again. That one check is what makes the whole thing safe to run while I keep working. Asking ImageMagick for a single pixel is enough:

magick out.png -crop '1x1+24+420' +repage -format '%[pixel:p{0,0}]' info:

Capturing a specific window by its ID would sidestep the stacking problem entirely, but on recent macOS enumerating window IDs requires Screen Recording permission for whatever process asks, and I couldn’t get that from a plain script. Floating the frame and verifying the result turned out to be simpler and more reliable.

A few finishing touches

A couple of smaller tweaks made the output look properly polished. GUI Emacs on macOS colors the native title bar according to the frame’s ns-appearance parameter, so I set it to light for light themes and dark for dark ones, and add ns-transparent-titlebar so the bar blends into the buffer background. It’s a tiny detail, but it makes the whole window feel like one piece. (It’s also the same trick that fixes an unreadable title bar under light themes, but that’s a story for another day.)

The other tweak was really a bugfix. Every so often a shot came out with a stray glyph or two at the top of the buffer, some redisplay artifact I never bothered to fully diagnose. Forcing a (redraw-display) right before I read out the frame geometry made it go away.

Other roads not taken

If you want to run this on a server or in CI, the story gets even better on Linux. You can start a virtual X display with Xvfb, run the Cairo Emacs build against it, and use x-export-frames: fully headless, perfectly reproducible, and with none of the “is the right window on top?” nonsense. That’s the setup I’d reach for if I ever wanted to generate these in a GitHub Action.

There are also plenty of other capture tools depending on your platform: ImageMagick’s import and the venerable scrot on X11, grim on Wayland, gnome-screenshot, and so on. Most of them can target a specific window, which is handy. One thing that won’t work is emacs --batch: batch mode has no GUI frame, so there’s nothing to photograph. You genuinely need a real, on-screen frame (or Cairo’s off-screen rendering).

Where this is headed

For now this lives as a small tools/ script inside my Batppuccin repo: a sample.el to display, an Emacs Lisp file to set up the frame, and a shell script to drive the capture and verification. It has already saved me a ton of tedium. Regenerating four flavors’ worth of screenshots is one command, and they come out identical every time.

But all of my theme ports have the same need, and the logic is almost entirely theme-agnostic. So I suspect I’ll eventually pull it out into a little standalone project: point it at a theme directory, hand it a sample file, and let it produce a consistent gallery for any theme. If that sounds useful to you as well, let me know. It might just nudge me into actually doing it.

Until then, I hope some of these ideas save you a bit of manual cropping. Automating the boring parts is what Emacs is all about.

-1:-- Automating Emacs Screenshots (Post Emacs Redux)--L0--C0--2026-07-03T07:00:00.000Z

Joar von Arndt: Automatically Find PGP Keys in Mu4e


I have found a peculiar delight in encryption, and particularly in using pgp-based asymmetric cryptography. This is especially useful for securing email communications, but doing so requires remembering to run M-x mml-secure-sign or mml-secure-encrypt in Mail Utilities For Emacs (Mu4e) before sending the message. For this reason I have been wanting to write a quick method that automatically adds signing capabilities and encryption only the recipient has a public key. Thankfully, I did not have to do so because of Nicolas Cavigneaux’s recent post Sign Always, Encrypt When Possible where he showcases his functins for doing exactly that. It prompted me to tweak it slightly, and to add the functionality to automatically discover public keys that I have yet to import.

Cavigneaux’s post sets up an elegant arrangement: email should always be signed with your pgp key, and you should “upgrade” from there to encryption only if your correspondent can handle encrypted mail and has shared their public key with you. This is done by using Emacs’ built-in Easy Privacy Guard (epg) tooling to compare the owners of the imported public keys on the machine with the list of recipients in an email.1 If you have the public key for all recipients, the message will be automatically encrypted — otherwise it will merely be signed to prove that you are the author.

This was exactly what I had wanted, and I quickly incorporated this functionality into my own configuration. Testing it out, I felt a bit uneasy sending encrypted mail automatically. Because it is not the default, I felt that fully automated encryption made me unsure whether the mail has been encrypted or not — even when I know I have a public key. I thus made the slight modification of prompting for both encryption and signing:

(defun kudu/message-sign-encrypt-if-all-keys-available ()
    "Add MML tag to encrypt message when there is a key for each recipient,
sign it otherwise."
    (if (bounga/message-all-epg-keys-available-p)
        (if (y-or-n-p "Encrypt? ")
            (mml-secure-message-sign-encrypt)
          (when (y-or-n-p "Sign? ")
            (mml-secure-message-sign)))
      (when (y-or-n-p "Sign? ")
        (mml-secure-message-sign))))

In practice I will probably always be pressing Y when prompted, but this adds a level of certainty that the code is running correctly. It reminds me of an advertising gimmick for housewives in the 1950s: when cake mix first became available, it was not appreciated because it was perceived as being “too easy” — it did not feel like baking a cake yourself. And so the recipe was tweaked to simply require an extra egg to be added. In practice this was a trivial change, but it meant that you felt more responsible for the finished work.

But there is an additional step I want to automate that Cavigneaux alluded to, but did not implement. He writes:

[T]he policy is only as good as my keyring. Encryption depends on having imported the right public keys; nothing here fetches them for me.

I have recently been having more correspondence with people using the Swiss-based email provider Protonmail. Proton provides support for the Web Key Directory (wkd) method of providing keys, where the dns settings of a domain point to a file containing a public key for a specific user. This allows the email itself to “provide” its own public key for encryption. The public key of any @protonmail.com or @pm.me address can be quickly imported in a second using gpg --locate-external-keys email@pm.me. Proton will similarly use wkd to get your public key, and so you will receive encrypted mail as well. This is still easy to set up for any other email provider, as long as you can edit the domain settings.2

Sadly, epg does not come with any options for Gnupg’s --locate-keys or --locate-external-keys flags. This might be a good addition. In the meantime, we can write a quick and dirty function to do the job for us in the background when opening any new email:

(defun kudu/message-locate-keys ()
  "Tries to find the public keys of 'bounga/message-recipients' via WKD through --locate-keys."
  (dolist (recipient (string-to-list (bounga/message-recipients)))
    (let ((recipient-email (cadr recipient))
          (proc (make-process
                 :name "gpg-locate-keys"
                 :command (list epg-gpg-program "--no-tty" "--locate-keys" (cadr recipient))
                 :connection-type 'pipe
                 :filter (lambda (proc string)
                           (process-put proc 'output
                                        (concat (or (process-get proc 'output) "") string)))
                 :sentinel (lambda (proc event)
                             (when (eq (process-status proc) 'exit)
                               (let ((output (process-get proc 'output))
                                     (email (process-get proc 'email)))
                                 (if (and output (string-match-p "imported: [1-9]" output))
                                     (message "Public key imported for %s" email))))))))
      (process-put proc 'email recipient-email))))

We use --locate-keys because the stricter --locate-external-keys will check the domain even if a key already exists in the local keyring. --locate-keys will instead instantly find local keys and finish early, while only missing keys will be checked. This is done asynchronously through make-process so that it does not cause the ui to freeze as we wait for network requests to finish. gpg seems to check very quickly, so it is not a large performance hit regardless.

We want to have imported any new keys before sending our email because Cavigneaux’s functions are run on 'message-send-hook. We therefore run kudu/message-locate-keys much earlier:

(add-hook 'mu4e-view-rendered-hook 'kudu/message-locate-keys)

This adds any correspondents’ public keys on any opened email, and so does not have to go through your entire contacts list. A downside is that it does not check for a public key when you are the person sending an unsolicited email, but it will when you get their reply.

This is not the world’s most elegant function, and if someone more familiar with epg has any comments regarding ways to improve it I will happily edit this post for improvements. Just send me a (pgp encrypted!) email. Run echo 'am9hcnhwYWJsb0B2b25hcm5kdC5zZQ' | base64 --decode | xargs gpg --locate-external-keys to import my key. If you have wkd set up, I will automatically fetch your public key. If not, this is a great opportunity to fix that!

I will also note how wonderful Emacs and epg make working with encrypted files, messages, and/or text more generally. I must admit that I used a plain-text .authinfo for an embarrassingly long time to store some of my secrets, thinking it would be bothersome to use the encrypted .authinfo.gpg for auth-sources. If you are like me, do not worry — the peace of mind from being able to write down personal information and simply leave it scattered on your filesystem is well worth just having to type your password.

Footnotes:

1

From Cavigneaux’s post, the function to get all message recipients. Returns a list formatted like (("First recipient" "first@domain.tld") ("Second recpient" "second@domain.tld") ...):

(defun bounga/message-recipients ()
  "Return a list of all recipients in the message, looking at TO, CC and BCC.
Each recipient is in the format of `mail-extract-address-components'."
  (mapcan (lambda (header)
            (let ((header-value (message-fetch-field header)))
              (and
               header-value
               (mail-extract-address-components header-value t))))
          '("To" "Cc" "Bcc")))
2

So no @gmail.com, @outlook.com, or @yahoo.com addresses. You can still use these to service your mail; wkd does not interact with any email-related dns records.

-1:-- Automatically Find PGP Keys in Mu4e (Post Joar von Arndt)--L0--C0--2026-07-02T22:00:00.000Z

Irreal: Magit 4.6 Is Released

Jonas Bernoulli is announcing the release of Magit 4.6. It includes, he says, over 300 commits since the last release 6 months ago. You can check Bernoulli’s post for the details but an overview is that the main changes were in

  • Blob handling. Working with blobs should be easier and make more sense with this release.
  • Experimental syntax highlighting in diffs.
  • Improved hook handling that allows for the calling of Elisp hooks from Git hooks.
  • Improved commit message processing. List of changed definitions in a diff are now available for composing a git commit message.

As I’ve said before, Magit and Org Mode are responsible for luring many new users into the Emacs event horizon so these improvements are important. Moreover, they raise again the need for supporting this effort. Bernoulli spends full time on Magit on a few other open source projects and depends on contributors for his livelihood Take a look at his support page to see how you can help out.

-1:-- Magit 4.6 Is Released (Post Irreal)--L0--C0--2026-07-02T14:46:33.000Z

Protesilaos: Emacs: write with input method (e.g. French) and Jinx for spelling

Raw link: https://www.youtube.com/watch?v=UjCbNOqUgTs

In this video demonstration I show the tools I use to write in French using Emacs. One is the built-in framework for input methods, which allows us to compose characters to express the full range of the language we are typing in (e.g. French or Greek). The other is a spell-checking package called jinx, which is developed by Daniel Mendler.

Below is the code I showed in the video:

(use-package emacs
  :demand t
  :bind
  ( :map global-map
    ;; The `toggle-input-method' sets the latest selected input
    ;; method, or the one defined in `default-input-method'.  Once an
    ;; input method is set, `toggle-input-method' will switch back to
    ;; the standard Emacs input.
    ("<f2>" . toggle-input-method))
  :config
  ;; Call the command `describe-input-method' to get a description of
  ;; what the given input method supports in terms of key sequences as
  ;; well as the key layout it has.
  (setq default-input-method "french-postfix"))



;; You need to have libenchant available on your system. For example,
;; Debian provides the package `libenchant-2-dev'.
(use-package jinx
  :ensure t
  :demand t
  :bind
  ( :map global-map
    ("M-$" . jinx-correct) ; or bind `jinx-correct-all'
    ("C-M-$" . jinx-languages))
  :config
  ;; Here you can specify a string with space-separated dictionaries.
  ;; I install the aspell dictionaries, such as the Debian package
  ;; `aspell-fr' for French and `aspell-el' for Greek (Éllinika).
  ;; With `aspell' installed on the system, do `aspell dicts' on the
  ;; command-line to get a list of available dictionaries.
  (setq jinx-languages "en fr el es")

  ;; I want to have Jinx in programming modes but I do not want it to
  ;; check anything that is a comment or string, because then it
  ;; underlines too many things which are not useful. We can do the
  ;; same for other modes, though I think this is fine.
  (setq jinx-exclude-faces
        '((prog-mode font-lock-comment-face font-lock-string-face)))

  (global-jinx-mode 1))
-1:-- Emacs: write with input method (e.g. French) and Jinx for spelling (Post Protesilaos)--L0--C0--2026-07-02T00:00:00.000Z

Nicolas Cavigneaux: emacs-mailto: Open macOS mailto: Links in Emacs

What it is

emacs-mailto makes macOS mailto: links open in Emacs, using whatever mail-user-agent you already have configured: mu4e, notmuch, Gnus, Rmail, or plain message-mode. Click a mailto: link in your browser, and Emacs comes to the front with a compose buffer already addressed.

The project lives at sr.ht/~bounga/emacs-mailto, where you’ll find the source and the ticket tracker.

The problem it solves

The GNU Emacs NS port (as shipped by emacs-plus, emacs-mac, or the official build) does not handle mailto: URLs.

When Emacs is already running, clicking a mailto: link activates Emacs but the URL never arrives: macOS brings the app to the front and drops the URL Apple-event before it ever reaches Emacs Lisp. So nothing happens: no compose buffer, and no way to fix it from your config, because the event never gets there in the first place.

That’s the whole frustration emacs-mailto exists to remove.

How it works

emacs-mailto sidesteps the dropped Apple-event with a tiny relay app registered as your default mail application. On a mailto: click, it brings Emacs to the front and hands the URL to emacsclient:

open -a Emacs
emacsclient -n -e "(run-at-time 0 nil (lambda () (browse-url-mail \"<url>\")))"

browse-url-mail parses the mailto: URL (recipient, subject, body) and opens a compose buffer through your mail-user-agent. The run-at-time wrapper lets emacsclient return immediately instead of blocking on an interactive prompt, such as a From-address picker.

A few details make it painless in practice:

  • emacsclient is located automatically at runtime (Homebrew on Apple Silicon or Intel, MacPorts, ~/.local/bin, or /usr/bin), so no path is hard-coded.
  • The app is built and ad-hoc signed locally by the installer, so there is no Gatekeeper quarantine and no pre-built binary to trust.

Requirements

  • macOS
  • Emacs running with a server (M-x server-start, or the Doom/Spacemacs defaults), so emacsclient can reach it
  • A configured mail-user-agent (this is what actually opens the compose buffer)

Installation

Clone the repository and run the installer:

git clone https://git.sr.ht/~bounga/emacs-mailto
cd emacs-mailto
./install.sh

This compiles Emacs Mailto.app into ~/Applications, registers it, and sets it as the default mailto: handler.

Test it:

open "mailto:test@example.com?subject=Hi"

Emacs should come to the front with a compose buffer addressed to test@example.com.

If your Emacs app has a different name, or you want a custom bundle identifier, override them with environment variables:

EMACS_APP="Emacs" BUNDLE_ID="org.nongnu.emacs-mailto" ./install.sh

A caveat about the default mail app

The relay runs as a background agent (LSUIElement), so it does not show up in Mail → Settings → General → “Default email application”. That menu may keep displaying another app; ignore it. To check the real handler, ask LaunchServices directly:

swift -e 'import AppKit; print(NSWorkspace.shared.urlForApplication(toOpen: URL(string:"mailto:x@y.z")!)!.path)'

It should print .../Emacs Mailto.app.

Uninstall

./uninstall.sh

This removes the app and resets the mailto: handler to Apple Mail.

Wrapping up

The whole thing is one small relay that removes a long-standing papercut of running Emacs as a mail client on macOS. It’s MIT licensed. If you hit a rough edge with your particular Emacs build or macOS version, the tracker is on the project page at sr.ht/~bounga/emacs-mailto.

-1:-- emacs-mailto: Open macOS mailto: Links in Emacs (Post Nicolas Cavigneaux)--L0--C0--2026-07-01T22:00:00.000Z

Nicolas Cavigneaux: shpaste: a paste.sr.ht Client for Emacs

What it is

shpaste is a small Emacs client for paste.sr.ht, the pastebin service from SourceHut. It lets me share a snippet without leaving the editor: select some text, run a command, and the paste URL is already in my kill-ring, ready to paste into a chat or an email.

It is built on the SourceHut GraphQL API and does just enough to be useful: create pastes, list them, open them, delete them. Nothing more.

The project lives at sr.ht/~bounga/shpaste, where you’ll find the source, the ticket tracker, and the mailing lists.

What it does

  • Create a paste from the region or from the whole buffer. The resulting URL is pushed to the kill-ring and shown in the echo area.
  • Browse your pastes in a dedicated list buffer, sorted and tabulated.
  • From that buffer: open a paste, copy its URL, delete it, or refresh the list.
  • Choose a visibility per paste: public, unlisted, or private.
  • Point it at a self-hosted SourceHut instance if you don’t use the public one.

The list buffer looks like this:

The shpaste-list buffer

Requirements

  • Emacs 29.1 or later
  • plz (GNU ELPA) and curl

Installation

shpaste is not on MELPA yet; the package is only a few weeks old. MELPA Stable support will come later, built from the version tags. In the meantime, install it straight from the repository.

On vanilla Emacs 29.1+, package-vc-install clones and builds it from source:

(package-vc-install "https://git.sr.ht/~bounga/shpaste")

On Emacs 30+, you can let use-package do the same with :vc:

(use-package shpaste
  :vc (:url "https://git.sr.ht/~bounga/shpaste" :rev :newest))

In Doom Emacs, declare the recipe in packages.el:

(package! shpaste
  :recipe (:type git
           :host nil
           :repo "https://git.sr.ht/~bounga/shpaste"
           :files ("*.el")))

Then configure it in config.el. This is the setup I use, with a paste leader menu and the list-buffer bindings:

(use-package! shpaste
  :commands (shpaste-list
             shpaste-create-from-region
             shpaste-create-from-buffer)
  :init
  (setq shpaste-default-visibility 'unlisted)
  (map! :leader
        (:prefix ("P" . "paste")
         :desc "List my pastes"      "l" #'shpaste-list
         :desc "Paste buffer"        "p" #'shpaste-create-from-buffer
         :desc "Paste region"        "r" #'shpaste-create-from-region))
  :config
  (map! :map shpaste-list-mode-map
        :n "RET" #'shpaste-list-open
        :n "w"   #'shpaste-list-copy-url
        :n "d"   #'shpaste-list-delete
        :n "gr"  #'shpaste-list-refresh))

Getting a token

shpaste authenticates with a SourceHut OAuth2 personal access token. Generate one at meta.sr.ht/oauth2 with the PASTES grant in read-write mode, then store it in auth-source with the host set to your instance — for example in ~/.authinfo.gpg:

machine paste.sr.ht password <YOUR_TOKEN>

There is no shpaste-token variable: the token is only ever read from auth-source. If you want the details of how that lookup works and why I chose it, I wrote a whole post about it: Using auth-source in a Real Emacs Package.

Usage

Command Action
shpaste-create-from-region Create a paste from the region; URL to kill-ring
shpaste-create-from-buffer Create a paste from the whole buffer
shpaste-list Browse your pastes in a list buffer

Inside the shpaste-list buffer:

Key Action
RET Open the paste
w Copy its URL
d Delete it
g Refresh the list

Configuration

Two options, both optional:

  • shpaste-instance (default "paste.sr.ht") — set it to a self-hosted host. It is used both to build the API endpoint and as the auth-source lookup key, so the machine field above must match it.
  • shpaste-default-visibility (default unlisted) — one of public, unlisted, or private.

Wrapping up

That’s the whole tool. shpaste is young (currently 0.1.0) and MIT licensed. If you live in Emacs and use SourceHut, give it a try — and if you hit a rough edge or have an idea, the project page is at sr.ht/~bounga/shpaste.

-1:-- shpaste: a paste.sr.ht Client for Emacs (Post Nicolas Cavigneaux)--L0--C0--2026-07-01T22:00:00.000Z

Nicolas Cavigneaux: Sign Always, Encrypt When Possible: Automatic GPG in mu4e

Introduction

GPG is only useful for the emails you actually remember to protect.

In mu4e, signing and encrypting a message is a manual gesture. Before sending, I hit C-c C-m s p to sign, or C-c C-m c p to sign and encrypt. It works, but it relies on me thinking about it every single time.

I don’t. Nobody does.

So in practice, the manual approach means most messages go out unsigned, and encryption only happens on the rare occasion I stop to remember it exists.

I wanted the opposite default. Something with no friction, that follows a clear policy:

  • Sign every message I send. Always.
  • Encrypt a message when, and only when, I have a public key for every recipient.

The second rule matters. Encryption is all-or-nothing: if a single recipient is missing from my keyring, encrypting would lock them out of their own email. So encryption has to be opportunistic — on when it can be, silently off when it can’t.

Here is how I wired that into mu4e.

The Building Blocks

Three pieces do the work.

MML secure tags. When you compose a message, Emacs doesn’t encrypt anything itself. It inserts an MML tag that tells the message layer what to do at send time. Two functions add those tags for me: mml-secure-message-sign and mml-secure-message-sign-encrypt.

message-send-hook. This hook runs right before a message is sent, when the headers are filled in and the recipients are known. That’s the perfect moment to decide whether to sign or to sign and encrypt.

epg. The Emacs interface to GnuPG. I use it to ask a simple question: do I have a public key for this address?

Two settings configure the signing context:

(setq mml-secure-openpgp-sign-with-sender t
      mml-secure-openpgp-encrypt-to-self t)

sign-with-sender tells the OpenPGP layer to pick the signing key based on the From address, which is exactly what I want when juggling several accounts.

encrypt-to-self is the one that saves you from a classic mistake. When you encrypt a message to someone else’s key, you can no longer read it — not even in your own Sent folder. Encrypting to yourself as well keeps your sent copies readable.

Collecting the Recipients

To decide whether encryption is possible, I first need the full list of people the message is going to.

(defun bounga/message-recipients ()
  "Return a list of all recipients in the message, looking at TO, CC and BCC.
Each recipient is in the format of `mail-extract-address-components'."
  (mapcan (lambda (header)
            (let ((header-value (message-fetch-field header)))
              (and
               header-value
               (mail-extract-address-components header-value t))))
          '("To" "Cc" "Bcc")))

The bounga/ prefix is my personal namespace. Emacs Lisp has a single global namespace, so prefixing your own functions is the convention that keeps them from colliding with built-ins or package code — never define a bare message-recipients that looks like it belongs to message.el. Pick whatever prefix you like; just be consistent.

I walk the To, Cc and Bcc headers, read each one with message-fetch-field, and parse it with mail-extract-address-components. The trailing t asks for all addresses in a header, not just the first, so a To line with five people returns five entries.

Bcc is in the list on purpose. A blind-carbon recipient still has to be able to read the message — if I can’t encrypt to them, I can’t encrypt at all.

mapcan concatenates the per-header results into a single flat list.

Do I Have Everyone’s Key?

Now the actual question: is there a public key in my keyring for every recipient?

(defun bounga/message-all-epg-keys-available-p ()
  "Return non-nil if the pgp keyring has a public key for each recipient."
  (require 'epa)
  (let ((context (epg-make-context epa-protocol)))
    (catch 'break
      (dolist (recipient (bounga/message-recipients))
        (let ((recipient-email (cadr recipient)))
          (when (and recipient-email (not (epg-list-keys context recipient-email)))
            (throw 'break nil))))
      t)))

I create an epg context, then loop over the recipients. For each one I ask epg-list-keys whether the keyring holds a key for that address. The email is the second element of each parsed recipient, hence cadr.

The logic is deliberately strict. The moment I find a single recipient with no key, I throw out of the loop and return nil: encryption is off the table. Only if the loop completes — every recipient covered — does the function return t.

The catch/throw pair is just an early exit. There is no point checking the remaining addresses once one is missing.

Tying It to Send

The decision function is tiny, because the two helpers did the hard part:

(defun bounga/message-sign-encrypt-if-all-keys-available ()
  "Add MML tag to encrypt message when there is a key for each recipient,
sign it otherwise.
Consider adding this function to `message-send-hook' to
systematically send signed / encrypted emails when possible."
  (if (bounga/message-all-epg-keys-available-p)
      (mml-secure-message-sign-encrypt)
    (mml-secure-message-sign)))

If I have everyone’s key, sign and encrypt. Otherwise, just sign. Either way, the message is signed — that part is never optional.

The last step is to run this before every send:

(add-hook 'message-send-hook 'bounga/message-sign-encrypt-if-all-keys-available)

message-send-hook fires once the message is complete and about to leave. By then the recipients are settled, so the keyring check reflects exactly who will receive the message.

The Result

With those few lines in place, I no longer have to remember GPG at all.

Every email I send is signed. Whenever I happen to have public keys for all the recipients (which, over time, covers more and more of my correspondence), the message is encrypted too, without a single keystroke on my part.

A couple of honest caveats.

Signing everything means every recipient can tell I use GPG, and gets a signature attachment whether they care about it or not. For me that’s a feature, not a cost.

And the policy is only as good as my keyring. Encryption depends on having imported the right public keys; nothing here fetches them for me. When a key is missing, the message silently falls back to signing only — which is the safe failure mode, but a silent one. If you’d rather be told, this is the spot to add a notice.

None of that changes the day-to-day feel, though: I write email, I send it, and the right cryptography happens on its own.

BTW, my public PGP key is available here: DB19B66E.

-1:-- Sign Always, Encrypt When Possible: Automatic GPG in mu4e (Post Nicolas Cavigneaux)--L0--C0--2026-07-01T22:00:00.000Z

Meta Redux: Projectile 3.0

Projectile 3.0 is finally out, and it’s a big one - easily the biggest release in years. There’s a nice reason for that: this year Projectile turns 15.1 It all started back in the summer of 2011, simply because I was frustrated that find-file-in-project didn’t work on Windows, and somehow that little anniversary made me want to shake things up and finally tackle a whole pile of changes I’d been putting off for ages. Some new features, some long-overdue spring cleaning, and a few things I’d wanted to remove for the better part of a decade.

What’s dead can’t die

There’s a running joke that Projectile is obsolete now that Emacs ships project.el out of the box. I’ve heard it many times, and I even wrote about it when Projectile turned 10 - it’s obviously hard to compete with a built-in package, as it has a home-field advantage you can never match.

But here’s the thing. If it’s really true that Projectile is dead and everyone has moved on to project.el, then that’s oddly liberating. What’s dead can’t die, right? If there are no users left to upset, I can finally go wild with my wildest ideas and stop worrying about breaking the workflows of Projectile’s (non-existent) users. And that’s more or less the spirit in which I approached 3.0 - I stopped being quite so precious about backwards compatibility and just did what I felt was right for the project.

(Turns out there might still be a user or two out there after all, so do skim the migration notes below. Sorry in advance!)

The highlights

There’s a lot in this release - well over a hundred entries in the changelog - but here are the highlights I’m most excited about:

  • projectile-dispatch (bound to s-p m) is a new transient menu that mirrors the command map, so you no longer have to remember every keybinding. It also exposes command modifiers as switches, so you can toggle a regexp search, start a fresh shell, invalidate the cache, or open a result in another window/frame right from the menu.
  • Projectile can now index a project asynchronously, in a background process, so a cold projectile-find-file no longer freezes Emacs while it walks a huge tree. The building blocks are public, so external tools can drive Projectile’s indexing asynchronously as well.
  • There’s a new (optional) projectile-consult integration built on those async data sources. Its finder streams candidates into the minibuffer as the project gets indexed, instead of blocking until indexing is done, while still honouring your VCS and indexing configuration.
  • The various search commands got folded into a single projectile-search, backed by a small, extensible backend registry. It ships grep, ripgrep and ag backends out of the box, and you can register your own (deadgrep, consult-ripgrep, whatever you fancy) in a few lines.
  • The shell, REPL and terminal commands got the same treatment - one projectile-run with shell, eshell, ielm, term, vterm, eat and ghostel backends, plus a seam to plug in your own.
  • The indexers got a thorough going-over - batched stat calls, single directory listings instead of dozens of file-exists-p probes, smarter caching. It all adds up, and it really shows over TRAMP, where every needless round-trip used to hurt. I think that 15 years later I’ve finally solved Projectile’s infamous performance issues over TRAMP!2
  • I finally removed a pile of features that had outlived their usefulness - the single-key Commander (superseded by the dispatch menu), the idle timer, projectile-browse-dirty-projects, and all of the built-in tags support (xref and LSP do this far better nowadays).
  • The minimum supported Emacs is now 28.1. That let me drop a heap of compatibility shims and finally make transient a proper dependency instead of an optional extra.
  • Projectile is a better project.el citizen now - it implements more of the protocol (project name, buffers, ignores), so tools built on top of project.el behave correctly in Projectile-managed projects.
  • I also dropped the legacy ido/ivy/helm completion systems - these days everything rides on the standard completing-read, so Vertico, Consult and friends just work. ido fans are the one group that has to lift a finger: install ido-completing-read+ (the package formerly known as ido-ubiquitous) and turn on ido-ubiquitous-mode, and ido will drive Projectile’s prompts just like before.
  • And there’s a pile of smaller quality-of-life additions - subproject compile/test for multi-module projects (c m c / c m t), a %p placeholder for the project name in command strings, projectile-add-and-switch-project, jumping back to the most recent project, and more.

Upgrading

3.0 is a major release for a reason - there are breaking changes. The minimum Emacs version is now 28.1, several long-deprecated commands and options are gone, and a couple of keybindings moved around. I’ve written a dedicated Migrating to Projectile 3.0 guide that walks through everything and how to adapt your config. Most setups will keep working untouched, but please give it a read before you upgrade.

Fifteen years later

As I said back when Projectile turned 10 - it’s always hard to compete with built-in packages. Still, I’m genuinely proud that 15 years in, Projectile is still here and, hopefully, still relevant to at least some of you. It remains one of my favourite projects and the one I reach for every single day.

Huge thanks to everyone who has contributed code, reported bugs, written extensions, or simply used Projectile over the years - none of this would exist without you. I really hope 3.0 isn’t the end of the innovation for Projectile, and that it’ll keep surprising and delighting its users for years to come.

Keep hacking!

  1. Give or take - I started Emacs Prelude the same year, so it’s always been a little hard to say which of my open-source projects was truly first. In my mind it’s always been Projectile. 

  2. Famous last words… I know… 

-1:-- Projectile 3.0 (Post Meta Redux)--L0--C0--2026-07-01T17:21:00.000Z

Irreal: Deleting To Trash Redux

Over at Emacs Dwelling there’s a nice post on why you should set Emacs to delete files by moving them to trash. That’s easy to do by setting delete-by-moving-to-trash to t. The post has a story illustrating why you should do that: it’s very easy to accidentally delete a file with Dired and if you do it to a file with changes that haven’t been committed to version control you can lose a few hours work. If you don’t use version control, the story is even worse.

The post seemed to me to be a worthy cautionary tale and I thought it would make a good Irreal post but when I began the writing process by entering a file name for the post, delete-to-trash.org, I discovered that such a file already existed from 2022. Even worse, for my blogging plans, it said everything I was planning to say in this post. Nevertheless, that was 4 years ago and the lesson, it seems, is evergreen so it’s worth making the point again. It costs you nothing and in those rare occasions where errant muscle memory causes you to delete a file, it can save the day. So rather than repeat the story I’ll just suggest that you check out the Emacs Dwelling post or my previous effort.

-1:-- Deleting To Trash Redux (Post Irreal)--L0--C0--2026-07-01T15:13:19.000Z

Meta Redux: Sayid Redux

I have a soft spot for Sayid - it’s one of the most ingenious Clojure tools ever built, and also one of the most neglected. It’s an omniscient debugger: instead of stopping your program at a breakpoint, it quietly records every call to the functions you’ve traced and lets you rummage through the recording afterwards. It’s the kind of thing you demo to people and watch their jaw drop. And it had been sitting practically unmaintained for the better part of a decade.

Here’s the awkward part: that neglect is largely on me. Bill Piel, Sayid’s original author, handed me the keys ages ago, and I’ve been… let’s say a less than exemplary steward. I’d merge the occasional patch to keep the lights on, but until very recently I’d done precious little to actually move the project forward.

The best time to maintain your open-source project was six years ago. The second best time is now.

– Ancient proverb, lightly adapted

So what finally lit a fire under me? Blame CIDER 2.0. I’ve been reworking CIDER’s debugging and tracing story, and at some point I sat down to make the built-in tracer smarter. A few hours in it hit me: nothing I could realistically bolt onto the built-in tracing would come anywhere close to what Sayid already does. So instead of building a worse Sayid, I dusted off the real one, gave it a good scrub, and here we are - Sayid 0.4.

What is Sayid, anyway?

For the uninitiated: Sayid records the arguments, return value, timing and full call tree of the functions you trace, so you can go back and inspect exactly what happened - no breakpoints, no println, no re-running the thing five times.

While we’re on the subject of embarrassing confessions: I’ve been the maintainer of this thing for years and I still have no idea what “Sayid” actually means or what it’s a reference to. If you happen to know, please, put me out of my misery - I’d love to finally get the joke.

Trace a namespace, run your code, and pop open the workspace with C-c s w:

▾ (demo.coins/can-afford? [:quarter :dime :nickel :penny] 45) => true
    (demo.coins/total-cents [:quarter :dime :nickel :penny]) => 45

can-afford? says true, but four coins worth 41 cents shouldn’t cover a 45-cent tab. Something’s off, and the bug lives inside total-cents. This is where Sayid really shines - flip on an inner trace and it records every expression inside the function:

▾ (demo.coins/can-afford? [:quarter :dime :nickel :penny] 45) => true
  ▾ (demo.coins/total-cents [:quarter :dime :nickel :penny]) => 45
    ▾ (apply + (map coin-values coins)) => 45
        (map coin-values coins) => (25 10 5 5)

There it is, staring right at us: (25 10 5 5), when the last value should be 1. A penny is worth five cents in our coin map. We never wrote a single println, and we never had to guess where to put a breakpoint - we just looked at what actually happened.

That’s the pitch, and it’s a great one. So why was such a cool tool gathering dust?

Why an omniscient debugger, in 2026?

Honestly, part of it is that Clojure folks are spoiled. We have the REPL, so the reflex for most of us is to just re-run a form with some tap> or println sprinkled in and eyeball the output. That works right up until it doesn’t - until the bug is three layers deep in a map over a lazy seq, or only shows up on the 400th call, or lives in code you didn’t write and don’t feel like instrumenting by hand.

A traditional stepping debugger stops the world and makes you drive. Sayid does the opposite: it lets the program run to completion and then hands you the entire execution history as something you can navigate at your own pace. tools.trace is in the same spirit, but it dumps text to stdout - Sayid keeps structured data and gives you a query DSL to slice it. That’s a much better fit for how we actually work in Clojure: run it, capture everything, explore the data.

And here’s the thing that made me want to revive it rather than reinvent it - “capture everything as data and explore it later” is exactly the workflow that’s becoming more relevant, not less. Structured execution traces are gold, whether the thing doing the exploring is you, a data-inspection tool like Portal, or an AI assistant trying to understand why your code misbehaved.

Out with the old coordinates

First order of business was dragging the project into the present.

Sayid used to live under com.billpiel/sayid on Clojars, with namespaces like com.billpiel.sayid.core. The new home is clojure-emacs, so the artifact is now published as mx.cider/sayid:

{:user {:plugins [[mx.cider/sayid "0.4.0"]]}}

I also dropped the personal-domain prefix from every namespace - it’s plain sayid.core, sayid.trace, sayid.nrepl-middleware and so on now. The old com.billpiel/sayid coordinates still get the same releases for the time being, so nobody’s dependency breaks overnight, but the future is mx.cider.

Data first, strings later

Here’s the change I’m most excited about. Sayid’s nREPL middleware used to hand the Emacs client a pre-rendered blob of text plus a pile of text properties for colouring. In other words, the server did all the rendering and the client was a dumb terminal. That single decision is a big part of why there was exactly one Sayid client.

So the middleware now speaks data. There’s a family of new ops - sayid-get-workspace-data, sayid-query-data and friends - that return the recorded call tree as honest, navigable data instead of a wall of text:

{"id"       "4793"
 "name"     "demo.coins/can-afford?"
 "args"     ["[:quarter :dime :nickel :penny]" "45"]
 "return"   "true"
 "file"     "demo/coins.clj"
 "line"     12
 "children" [...]}

The structural bits (ids, names, timings, source location, the tree shape) come across as real nested maps and lists you can walk by key. The captured values are pr-str‘d, since an arbitrary Clojure value can’t always round-trip over the wire - but that’s the only place strings sneak in, and it’s exactly where you’d expect them.

The upshot: any editor or tool that speaks nREPL can now fetch a workspace and render it however it likes, and a REPL one-liner or a Portal tap gets you the same data with zero Sayid-specific machinery. The whole thing is written up in doc/nrepl-api.md, so you don’t have to reverse-engineer the wire format from the Emacs client the way you would have before.

A UI that doesn’t feel like 2015

With the data ops in place, I rebuilt the Emacs UI on top of them. The workspace and the “what’s traced” views are now proper foldable trees, built on CIDER’s new cider-tree-view. You get real folding (TAB), navigation (n/p), jump-to-source (RET), and - my favourite - c i hands the actual captured value straight to CIDER’s inspector, so you can drill into an argument or a return value as a live, navigable object rather than squinting at its printed form.

There’s also a query layer wired into the tree: f narrows to every recorded call of the function at point, i focuses a single call and its subtree. On a big trace that’s the difference between “wall of text” and “actually finding the thing”.

The best part is that the client is now smaller, not bigger - all the tree rendering, folding and value inspection are handled by mature components instead of bespoke code painting text properties by hand. That’s the payoff of moving rendering to the client: the server ships data, and the client is free to be as fancy or as plain as it wants.

Wait, you broke everything?

Backwards compatibility is a promise you make to the users you have. Sayid had exactly one, and reader, I am that user.

– Me, rationalising

Yeah, I did. I went pretty wild with the breaking changes this time around - new artifact coordinates, new namespaces, a reworked nREPL API, a bumped minimum CIDER version. Normally I’d bend over backwards to keep old clients working, but here I made a deliberate call: as far as I can tell, the bundled Emacs client was the only client Sayid ever had. Dancing around imaginary third parties to preserve compatibility nobody was relying on would have just made the project harder to adopt and harder to maintain.

So I opted for sweeping changes that leave Sayid in a much better place to build on, rather than a museum of backwards-compatible cruft. If it turns out I was wrong and you were quietly depending on the old coordinates - I’m sorry, and do let me know, because that’s genuinely useful information.

Take it for a spin

That’s the gist of it. Sayid is alive again, it’s leaner, it speaks data, and it has a UI I’m not embarrassed to demo. What I’d love now is for more people to actually use it and tell me whether the new direction resonates.

So please - [mx.cider/sayid "0.4.0"], trace something gnarly, and pop open the tree. Then head over to the issue tracker and tell me what you think: what feels great, what feels rough, what’s missing. I have ideas for where to take it next (bounding the recording so you can safely trace a whole namespace under a test suite is high on the list), but I’d rather steer by what people actually want out of it.

Big thanks to Bill Piel for building such a wonderful tool in the first place - I’m merely standing on the shoulders of a giant here. And thanks in advance to everyone willing to kick the tyres on the revival!

That’s all from me for now. Keep hacking!

P.S. One more thing…

Well, that didn’t take long. Remember that “high on the list” bit a few paragraphs up - bounding the recording so you can safely trace a whole namespace under a test suite? Turns out I couldn’t leave it alone. A short burst of small improvements after this post went up, and it’s done: Sayid 0.5.

That was the thing that made Sayid feel like a toy - point it at a real workload and it would cheerfully eat your whole heap and fall over. The recording now has a set of tunable bounds instead: a cap on how many top-level calls it keeps, a depth limit, 1-in-N sampling for hot paths, a per-function cap, and a keep-only-the-last-N mode for when what you care about is whatever happened right before things went sideways. Fat and infinite values no longer hang the data ops either, and the traced-functions view got its enable/disable/remove actions back.

The upshot for you: you can finally point Sayid at real code under real load without babysitting it. Same ask as before - give it a spin and tell me how it feels.

-1:-- Sayid Redux (Post Meta Redux)--L0--C0--2026-07-01T13:01:00.000Z

Chris Maiorana: God mode notes with Denote and Consult Notes

Consult notes is a package that hooks into your note-taking system. It works particularly well with Denote, picking up your notes directory variable automatically.

My interest in having a note system comes down to whether it helps my writing. A writer needs different kinds of notes: reading notes, notes on craft, notes on etymology, and random ideas. You’ll notice I’ve built that into the config below.

I’ve touched on the topic of note-taking in Emacs a few times, in posts on “making everything a note” and on the “undergrad” style of note-taking. I’ve often argued that a full dedicated notes package was unnecessary when you could simply create files and link them together.

But this “consult notes” package made me want to give Denote another try and see how it feels. So far, I like it. I’m going to test drive this config for a few weeks and report back on how it’s going.

Denote config

(use-package denote
  :ensure t
  :bind (("C-c n n" . denote)                ; new note
         ("C-c n f" . denote-open-or-create) ; find/create by title
         ("C-c n l" . denote-link)           ; insert a link to another note
         ("C-c n b" . denote-backlinks))     ; show what links here
  :custom
  ;; Names the destination for notes in the file system
  (denote-directory (expand-file-name "~/notes/"))
  ;; Default to Org type of files
  (denote-file-type 'org)
  ;; Keywords you reach for more often
  (denote-known-keywords '("craft" "etymology" "draft" "idea"))
  (denote-prompts '(title keywords))
  :config
  (denote-rename-buffer-mode 1))            ; show note title in the buffer name, nice

Consult

(use-package consult
  :ensure t
  :bind (("M-s l" . consult-line)            ; search within current draft
         ("M-s L" . consult-line-multi)      ; search across all open buffers
         ("M-s r" . consult-ripgrep)         ; search the whole writing directory
         ("M-s o" . consult-outline)         ; jump between org/markdown headings
         ("M-s b" . consult-buffer)
         ("M-s k" . consult-keep-lines)      ; isolate lines matching a pattern
         ("M-s f" . consult-flush-lines))    ; strip lines matching a pattern
  :custom
  ;; Small delay before previewing (keeps a big file from feeling jumpy)
  (consult-preview-key '(:debounce 0.2 any)))

Consult notes

(use-package consult-notes
  :ensure t
  :commands (consult-notes consult-notes-search-in-all-notes)
  :bind (("M-s n" . consult-notes)                       ; search notes by title
         ("M-s N" . consult-notes-search-in-all-notes))  ; full-text across notes
  :config
  ;; Reads denote-directory automatically (no second path to maintain)
  (consult-notes-denote-mode))

The post God mode notes with Denote and Consult Notes appeared first on Chris Maiorana.

-1:-- God mode notes with Denote and Consult Notes (Post Chris Maiorana)--L0--C0--2026-07-01T01:00:28.000Z

Raymond Zeitler: Create Date Books, Planners and Calendars with Emacs

Diary was my pick for this month's Emacs Carnival. But in fact it's part of a larger set of functions under the calendar group. Another set of functions allows you to create pocket date books, desk planners and wall calendars if you have access to a printer. The function names begin with cal-tex-cursor and cal-html-cursor, and they can be found in cal-tex.el and cal-html.el, respectively.

The "tex" and "html" in the function names describe the output formats, LaTeX and, well, HTML; "cursor" describes one way of defining their inputs (from the position of the cursor in the calendar buffer). They're further subdivided into layouts such as day, week, month and year. My favorite is cal-tex-cursor-week-iso.

Open the calendar (M-x calendar). Observe that the cursor is at today. Now invoke cal-html-cursor-month and accept the default at the prompt. The function will create a file named YYYY-MM.html. The variable cal-html-directory provides the cal-html functions with the path name. The HTML file is placed in a subdirectory named YYYY. In my case, cal-html-directory is set to "~/public_html." As I invoke it today, the subdirectory / filename is 2026/2026-06.html.

The calendar can pull information from your diary file(s), the holidays defined in holidays.el and in the holiday-other-holidays variable. If you have %%(diary-sunrise-sunset) and %%(diary-lunar-phases) you'll get the times of sunrise and sunset at your coordinates, and the dates and times of the four lunar phases, assuming that cal-tex-diary is non-nil. (I also have cal-tex-24 non-nil.)

I suggested cal-html-cursor-month because its output is easily viewed (rendered) with a web browser. But I prefer the weekly calendar produced by cal-tex-cursor-week-iso, which outputs in LaTeX.

I've invoked cal-tex-cursor-week-iso with the cursor on June 16. The output is shown in Figure 1 after rendering to PDF with pdflatex. (Note that the screenshot is closely cropped.) There's a lot included here.

The Rendered LaTeX for Week 25 of 2026

First, note that our Third Tuesday of the Month Club meeting (used as an example in a previous post) is shown. This comes from a diary entry; unfortunately org file events are ignored. (The agenda for this week is shown in Figure 2; note the event in red does not appear in the weekly calendar. This is another reason I favor recording appointments in the diary.)

Two other diary entries also are included: a record of when the kitchen cabinets were painted, plus a breakfast meeting with Howard.

Then you'll find two holidays, Islamic New Year and Father's Day. And because I have diary-lunar-phases in my diary, a moon phase also shows up. (I commented out the diary-sunrise-sunset statement because it adds the information to every day of the week, which makes the weekly planner look too cluttered.) Last but not least the Summer Solstice is included as a Sunday holiday along with the time it occurs.

The first line of each day block also shows (right justified) the day number (of the year) and the number of days remaining in the year. The Sunday block is quite crowded; the first line is wrapped, so the day number / days remaining field appears on the next line.

Sadly, any links in the diary that worked in Org Agenda do not work in the LaTeX output. You'll see instead the full syntax of the link, [URL][description] ].

I created an improved version of cal-tex-cursor-week-iso. Here are some of the changes:

  • inhibit sunrise/sunset data for all but the first day
  • switch to sans-serif font
  • color birthdays and anniversaries in blue
  • replace the non-ASCII characters (used for Bahai holidays)
  • offset left or right to allow for the binding of a year's worth of pages
  • more?

I uploaded it to Codeberg, but I need to clean it up a bit before I feel comfortable promoting it.

If you position the cursor on the last day of the year and invoke cal-tex-cursor-week-iso with a prefix argument of 53, you can create the pages for a personalized weekly planner for next year! Try it out!

-1:-- Create Date Books, Planners and Calendars with Emacs (Post Raymond Zeitler)--L0--C0--2026-06-30T21:05:58.956Z

James Dyer: That Moment Dired Eats Your File and why Trash Saves You

I was working on my diff-minimap package, deep in the weeds of fringe indicators and search highlights, and I did the thing. You know the thing. I was in Dired, accidentally, for some reason hit D to delete a filename (probably some malfunctioning muscle memory) and it was gone!

20260515110240-emacs--That-Moment-Dired-Eats-Your-File-and-why-trash-saves-you.jpg

Except it was my own source file. diff-minimap.el. The one I had been hacking on for the last hour. The one with all those uncommitted changes.

Now, in my head, I have a trash setup. I am sure I configured that somewhere, probably years ago, in some init file on some machine. But the machine I was sitting at? Nope. Emacs just did a straight delete, no safety net, no way back.

The file was tracked in git, so I could git checkout HEAD -- diff-minimap.el to get the committed version back. But the uncommitted work - the inline diff preview, the vc-diff positioning, the side-window hunk viewer - that was gone.

Emacs has a built-in variable, delete-by-moving-to-trash. When it is set to t, Dired (and other delete operations) move files to the system trash instead of permanently deleting them. On Linux that is ~/.local/share/Trash/files/, on macOS it is ~/.Trash/, on Windows it is the Recycle Bin.

It is one of those settings that seems obvious in hindsight but is easy to overlook because Emacs defaults it to nil. The reasoning is probably that Emacs is a serious tool for serious people who know what they are doing, so deleting means deleting?!

So I added it to my config:

(setq delete-by-moving-to-trash t)

That is it. One line. From now on, D in Dired moves to trash instead of deleting permanently and I will not have that sinking feeling again!

Anyway, back to diff-minimap.

-1:-- That Moment Dired Eats Your File and why Trash Saves You (Post James Dyer)--L0--C0--2026-06-30T09:17:00.000Z

Meta Redux: CIDER 2.0 is Brewing…

Normally I write these posts after a CIDER release is out, with the smug satisfaction of someone who has already tagged the version and updated the changelog. Today I’m flipping the script a little: the release isn’t out yet, but I’m too excited to keep quiet about it.

Here’s the thing. The next CIDER release was supposed to be 1.23. But as I kept piling change upon change, it slowly dawned on me that calling this “1.23” would be doing it a disservice. So I’m now strongly considering shipping it as CIDER 2.0 instead.

A different kind of release

The last few CIDER releases were on the conservative side - lots of important internal work, but relatively little that you’d actually notice day to day. This one is the exact opposite. It’s stuffed with user-visible changes, many of which have been sitting on the issue tracker and in the back of my mind for literally years.

Why now? Two reasons.

First, all that boring groundwork paid off. Some of you noticed I spent a lot of time recently backfilling tests and generally enriching the test suite. That wasn’t busywork - it was me building a safety net, so that finally making sweeping changes would be safe and pleasant instead of terrifying. Mission accomplished: I’ve been refactoring with abandon and sleeping just fine.

Second - and I know this is the controversial part - AI agents have been a genuinely great help. Not for writing CIDER for me (the design taste is still very much mine, thank you), but for quickly prototyping my half-formed ideas in different ways so I can see and feel them before committing to the approach I like best. Turns out “show me, don’t tell me” works wonders when the thing showing you can knock out a prototype in minutes. Boo me if you must, but when you’re the maintainer of so many projects and you have only so little time to spend on all of them that really makes a difference.

There’s a third ingredient worth mentioning: I’d been hammocking on most of this for months, quietly turning the designs over in the back of my mind while I was wrapping up CIDER 1.22. So while 1.22 itself took the better part of four months (and still shipped later than I’d have liked), the release right after it has been snapping together in a couple of weeks - the code was the easy part once the thinking was done. It helps that a lot of these changes close issues that had been gathering dust for years; the ClojureScript macroexpansion bug (#2099) was filed all the way back in 2017. Nothing beats hammock-driven development - even if it’s decidedly not the fastest way to ship anything.

OK, enough preamble. Let’s look at the goodies.

Keymaps you can actually discover

CIDER has a lot of commands, and historically the only way to find them was to memorize cryptic key chords or grep the manual. No more. Every command prefix now pops a transient menu (the same UI magit made famous), and there’s a top-level cider-menu that ties them all together:

Evaluate             Code              REPL & session           Diagnostics
 e Eval...            t Test...         j Insert into REPL...    r Trace...
 m Macroexpand...     n Namespace...    x Jack-in / connect...   p Profile...
 d Documentation...   w References...                            l Log...

Crucially, this isn’t a modal speed bump - your old muscle memory still fires instantly. C-c C-d C-d runs cider-doc as fast as ever; the menu only shows up if you pause after the prefix, wondering “wait, what else is in here?”

One tree view to rule them all

A bunch of CIDER’s browsers grew up independently and looked it. They now share a single, foldable tree-view widget - the namespace browser, the spec browser, and the new call-graph browsers all behave consistently (TAB to fold, n/p to move around). For example, exploring who calls a function:

who-calls clojure.core/reduce
  ▾ my.app.core/sum
      ▸ my.app.api/handler
  ▸ my.app.util/total

Expand a node and CIDER lazily fetches the next level. It’s a much nicer way to spelunk through a codebase than a flat list of strings.

Find usages, batteries included

Speaking of call graphs - CIDER now ships with genuinely capable cross-reference functionality out of the box. cider-who-calls/cider-who-is-called for the call graph, cider-who-implements for protocols and multimethods, plus xref-find-references (M-?) that searches your source and therefore finds usages even in code you haven’t loaded into the REPL yet.

The practical upshot: if you were reaching for clj-refactor.el or clojure-lsp mostly to find usages, you probably don’t need them anymore. (If you were using them for other things, carry on - we’re friends, not rivals.)

Much of the inspiration here came straight from swank-clojure and SLIME itself, which have offered this kind of cross-referencing for ages. Sometimes nothing beats revisiting the classics.

The debugging toolbox got a serious polish

This is the part I’m most happy about. Pretty much every “what is my code actually doing?” tool got some love.

The macro tooling is the headliner. The macroexpansion buffer finally grew a header line that shows the active expander and options, cycles namespace display and metadata in place, pulses the freshly-expanded form, and - at long last - says something useful when you point it at a special form or an unresolved symbol, instead of silently shrugging. Better still, there’s a brand new inline stepper (cider-macrostep, a Clojure spin on the venerable macrostep package) that expands macros right where they sit, one step at a time. Expandable sub-forms get underlined so you can hop between them with n/p, and every gensym is painted its own color, so you can finally follow where that g__1234 ends up. I did not expect macro debugging to be fun, and yet here we are.

Tracing got the same treatment. Traced calls no longer smear themselves all over your REPL output; they stream into a dedicated, foldable *cider-trace* buffer instead:

*cider-trace*
▾ (my.app/process {:id 7})
    ▸ (my.app/validate {:id 7})  => true
    ▸ (my.app/persist {:id 7})   => {:id 7, :saved? true}
  => {:id 7, :saved? true}

And because “wait, what did I even trace?” is a question I ask myself constantly, cider-list-traced and cider-untrace-all are now a keystroke away.

Enlighten (you remember it, right?) picked up some manners too. You can light up a single form with cider-enlighten-defun-at-point without flipping the global mode, and cider-enlighten-stop makes the whole thing vanish at once, instead of making you re-evaluate everything in penance. And for the tap> crowd, cider-tap opens a buffer that streams whatever you send to tap> and lets you crack any value open in the inspector with RET. It’s println debugging, minus the println guilt.

ClojureScript gets some love too

ClojureScript is always the trickier sibling, but it got meaningful improvements this round:

  • You can now run ClojureScript tests with the regular test commands (including async cljs.test tests) instead of CIDER refusing to play along.
  • Macroexpansion of user-defined cljs macros finally works (it used to silently echo the form back unexpanded - a bug that had been open since forever).
  • And when you invoke a Clojure-only command under a cljs REPL, CIDER now tells you so clearly, rather than failing in some confusing way.

This was another area where AI tooling was quite helpful to me, as I’ve rarely used ClojureScript in the past, but I still managed to figure out how to finally solve those problems that have been pain points for CIDER’s users for as long as we’ve had ClojureScript support.

On a related note, I’ve also been chasing down a few small bugs in Piggieback (the middleware that powers most of the cljs REPLs CIDER talks to) - have a look at the recent releases if you’re curious. And I’ve been idly pondering some form of “native” shadow-cljs support down the road. That last one is very much TBD, so don’t hold me to it - but the ClojureScript story keeps inching forward.

A pile of quality-of-life touches

It wouldn’t be a CIDER release without a long tail of small comforts. A few that I keep bumping into and grinning at:

  • Eldoc is asynchronous now, so dragging the cursor around no longer blocks Emacs on an nREPL round-trip. Buttery smooth.
  • A new cider-mode lighter (with an optional fringe marker) flags when the current buffer’s namespace isn’t loaded into the REPL, or has gone stale because you edited an already-evaluated form. That retires a whole genre of “why isn’t my change taking effect?” head-scratching.
  • cider-doc can pull ClojureDocs examples right into the doc buffer.
  • Sending a form into your namespace’s (comment ...) block is a one-keystroke affair now (cider-send-to-comment), and cider-jump-to-comment teleports you back there.
  • Stuck in .cljc land? You can pin where a buffer’s evaluations go - clj, cljs, or both - with cider-set-eval-destination and friends.
  • The REPL banner is slimmer and far less shouty; the getting-started spiel now lives in a summonable reference card (C-c C-h).
  • The cheatsheet finally learned about all the functions Clojure has grown since 1.11.

I’m probably still forgetting a dozen things - the changelog is genuinely enormous this time around.

Please go play with it!

Unfortunately no one can be told what CIDER 2.0 is – you have to experience it yourselves…

– Clorpheus

Here’s where you come in. All of this is already available in the snapshot release of CIDER on MELPA. I’d love for you to install it, kick the tires, and - especially - tell me how the various UX changes feel. Discoverable? Annoying? Joyful? I genuinely want to know before I carve any of it in stone.

If no serious problems surface, I plan to cut the real release fairly soon - think a week or two. So now’s the perfect time to influence it.

Epilogue

I keep hearing that the Clojure community isn’t innovating much these days. I hope CIDER 2.0 goes a small way towards convincing the doubters that we’re not quite done yet. The best is always yet to come - for both Clojure and Emacs.

Now let’s go forth and brew some (magic) CIDER together! Keep brewing!

-1:-- CIDER 2.0 is Brewing… (Post Meta Redux)--L0--C0--2026-06-30T09:00:00.000Z

tusharhero: Underappreciated builtin: Grand Unified Debugger

~1089 words. ~5 minutes.

Or as I would like to call it, GLORIOUS Unified Debugger.

This is my submission for June's Emacs Carnival, Underappreciated Emacs built-ins.

You can find detailed information in the excellent documentation at (info "(emacs) Debuggers").

Multi debugger support

I mostly use its GDB Graphical Interface. But it supports multiple debuggers such as,

  • lldb (LLVM debugger)
  • perldb (Perl debugger)
  • jdb (Java debugger)
  • pdb (Python debugger)
  • guiler (Guile!)
  • dbx (Debugger with support for C, C++, and so.)
  • xdb (Debugger for MS Windows (?))
  • sdb (System Debugger)

The GDB interface

The manual has a entire section of the special GDB GUI interface. Which I will illustrate now.

This is our demo source file: fibo.c,

 #include  

 int
 fibo ( int  n)
{
   if (n < 2)
     return 1;
   else
     return fibo (n - 1) + fibo (n - 2);
}

 int
 main ( void)
{
   for ( int  i = 0; i < 5; i++)
    printf ( "%d: %d\n", i, fibo(i));
}

Firstly, we compile via,

gcc -g fibo.c -o fibo

(The -g flag is necessary for debugging with GDB.)

Now that we have a compiled program, we can try executing it.

./fibo
0: 1
1: 1
2: 2
3: 3
4: 5

Now we can proceed with starting gdb gud in Emacs. To do so, we type M-x gdb RET fibo RET, and this creates a buffer called *gud-fibo*.

Current directory is /home/tusharhero/Documents/c-scratch/
GNU gdb (GDB) 17.2
Copyright (C) 2025 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-unknown-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
    .

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from fibo...
 (gdb) 

Though we won't be directly using this CLI interface, you can use it if you want to.

Let's set some breakpoints.

GDB breakpoints by clicking on the fringe.

Yup, you can just click on the fringe (the place left to the text), and automatically set up the breakpoints, which show up like so in *gud-fibo*.

Breakpoint 1 at 0x401138: file fibo.c, line 7.
Breakpoint 2 at 0x40113f: file fibo.c, line 9.
Breakpoint 3 at 0x401174: file fibo.c, line 16.
 (gdb)  

Now, we will start debugging by running the program. tool-bar-run-button.jpg

These buttons: Run, Next Line, Step Line, Up Stack, and Down Stack are part of the tool bar.

And immediately it stops at the first breakpoint it encounters, (breakpoint 3). The white arrow represents the current line that is being executed.

fringe-current-line-arrow.jpg

Now we can step into fibo function by using Step Line.

tool-bar-step-line.jpg

Notice that we also have a new button for Continue now, which continues until the next break point is hit. So, let us turn off the breakpoint in the for loop. For this we will use the dedicated breakpoints management window.

Now let's just spam the continue button a bunch of times.

And since we are calling printf in the for loop, there is a pop out dedicated window for I/O. (Notice that it is not just for output).

Well what if I want to see what the value of n is currently? There are actually a bunch of ways to look at that.

Here you just kinda put your cursor over the thing you want to get the value of … and you just get it? :)

The other way to use to the locals window,

But then who wants to keep track of this manually? Let's just watch the variable, so that we get update whenever it changes.

Closing

I would like to cover many other features, but I unfortunately don't have the time to get through all of them, maybe I will write a sort-of sequel covering more features and other debuggers supported by GUD.

-1:-- Underappreciated builtin: Grand Unified Debugger (Post tusharhero)--L0--C0--2026-06-29T18:30:00.000Z

Sacha Chua: 2026-06-29 Emacs news

This week, lots of people were talking about FSF's policy of not accepting LLM contributions to Emacs core (see the last two items in the AI category). Comments seem generally supportive of FSF's caution.

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!

View Org source for this post

You can e-mail me at sacha@sachachua.com.

-1:-- 2026-06-29 Emacs news (Post Sacha Chua)--L0--C0--2026-06-29T17:04:18.000Z

Raymond Zeitler: Emacs disabled Commands

It's possible to configure Emacs to prompt the user before calling a function, thereby "disabling" a function. The function isn't totally inaccessible -- Emacs will let you run it if you want it to. I chose to do this with scroll-left, scroll-right, scroll-up-command and scroll-down-command as explained in the previous post.

Most keypresses call self-insert-command, which is itself a function. So what would happen if self-insert-command were disabled?

It turns out to be not as debilitating as you'd think -- only the alpha-numeric and punctuation keys are affected. When a modifier is used with a key (M-x, for example), there is no interruption. Likewise, the TAB key is allowed to pass, which makes completion easier.

When Emacs inhibits self-insert-command, it prevents the keypress from having its intended effect. It will say that the command is disabled because many users find it confusing. Then you'll have the options of:

  • (n) Not running the command
  • (y) Run the command and don't ask again
  • SPC Run the command just one time to try it
  • (!) Enable the command and all other disabled commands

The n, y, SPC and ! keys are not inhibited at this point, so the user doesn't get trapped in a set of recursive prompts.

So to invoke org-agenda, I'd press M-x o. Then I'd press SPC to enable the self-insert-command for "o." Then I'd press r followed by SPC, and so on. My keypresses are M-x o r g - a g TAB ENTER.

Thus, adding (put 'self-insert-command 'disabled t) to a cube-mate's init file is a harmless prank, something to try on April Fool's Day.

-1:-- Emacs disabled Commands (Post Raymond Zeitler)--L0--C0--2026-06-29T02:40:43.489Z

Protesilaos: Emacs: fontaine version 3.1.0

Fontaine allows the user to define detailed font configurations and set them on demand. For example, one can have a regular-editing preset and another for presentation-mode (these are arbitrary, user-defined symbols): the former uses small fonts which are optimised for writing, while the latter applies typefaces that are pleasant to read at comfortable point sizes.

Below are the release notes.


Version 3.1.0 on 2026-06-29

This is a small release that includes internal refinements as well as two user-facing changes:

  1. The user option fontaine-presets accepts an optional :line-spacing entry, which corresponds to the line-spacing variable. As of Emacs version 31, line-spacing can be bound to a cons cell to set the space above and below. Fontaine now handles this as intended.

  2. By default, changing the font size has the effect of resizing the frame. This is because of the original value of the variable frame-inhibit-implied-resize. Fontaine is now designed to always inhibit frame resizing, regardless of frame-inhibit-implied-resize.

-1:-- Emacs: fontaine version 3.1.0 (Post Protesilaos)--L0--C0--2026-06-29T00:00:00.000Z

Monadic Sheep: Canvas patch: we need testers!

by Tushar
2026 June 29

The Canvas patch is almost done. Except that we need more testing, specifically for its MS Windows port. (Though testers on other operating systems like GNU/Linux and MacOS are also welcome.)

Since we don't have any computers running MS Windows, we are not sure if the code is actually correct.

Please follow the following instructions for testing it on MS Windows. You can discuss the results or ask us questions at MonadicSheep Emacs Fork's Issue tracker, #phi-mu-lambda on Libera IRC (open webchat in browser), or on the fediverse (tag the post with #emacs).

1. Building GNU Emacs on MS Windows

More detailed information is available here.

  1. Install MSYS2 by following the official instructions mentioned there.
  2. Open a MSYS2 UCRT64 session terminal.
  3. In the bash prompt, run the following commands to get the canvas patch source code.

    pacman -Sy git
    git clone https://codeberg.org/MonadicSheep/emacs
    
  4. Now, change directory to emacs source checkout, by running the following command.

    cd emacs/
    
  5. Install dependencies,

    pacman -Sy --needed base-devel autoconf \
    mingw-w64-ucrt-x86_64-toolchain \
    mingw-w64-ucrt-x86_64-xpm-nox \
    mingw-w64-ucrt-x86_64-gmp \
    mingw-w64-ucrt-x86_64-gnutls \
    mingw-w64-ucrt-x86_64-libtiff \
    mingw-w64-ucrt-x86_64-giflib \
    mingw-w64-ucrt-x86_64-libpng \
    mingw-w64-ucrt-x86_64-libjpeg-turbo \
    mingw-w64-ucrt-x86_64-librsvg \
    mingw-w64-ucrt-x86_64-libwebp \
    mingw-w64-ucrt-x86_64-lcms2 \
    mingw-w64-ucrt-x86_64-libxml2 \
    mingw-w64-ucrt-x86_64-zlib \
    mingw-w64-ucrt-x86_64-harfbuzz \
    mingw-w64-ucrt-x86_64-libgccjit \
    mingw-w64-ucrt-x86_64-sqlite3 \
    mingw-w64-ucrt-x86_64-libtree-sitter
    
  6. Now, run the following command to build Emacs.

    make
    

    This may take a while depending on your hardware.

  7. Once that is done, you should be able to start emacs with the following command.

    ./src/emacs
    

2. Testing

Evaluate the following in *scratch* (Using C-c C-c keybinding).

(defun make-rect (width height pixel)
  (make-vector (* width height) pixel))

(setq rect-canvas-vec (make-rect 250 250 #xFFFF0000))
(setq rect-canvas `(image :type canvas
                          :data-width 250
                          :data-height 250
                          :data ,rect-canvas-vec))
(insert (propertize "#" 'display rect-canvas))

(defvar rect-canvas-timer nil)
(let ((i 0))
  (setq rect-canvas-timer
        (run-with-timer
         0 0.016
         (lambda ()
           (if (< i (* 20 250))
               (progn
                 (aset rect-canvas-vec (+ (* 115 250) i) #xFF0000FF)
                 (canvas-refresh rect-canvas t)
                 (setq i (1+ i)))
             (cancel-timer rect-canvas-timer))))))

You should be able to see something like this.

-1:-- Canvas patch: we need testers! (Post Monadic Sheep)--L0--C0--2026-06-29T00:00:00.000Z

Irreal: Linking A Diary Event To An Org Heading

Ray Zeitler has a useful post that describes how to solve a niche problem involving Emacs diary and Org mode. The problem is you have an event scheduled in your diary and you want access to an Org heading for clocking and note taking.

It turns out that it’s pretty easy to arrange that. Take a look at Zeitler’s post to see the exact recipe.

-1:-- Linking A Diary Event To An Org Heading (Post Irreal)--L0--C0--2026-06-28T14:33:03.000Z

Protesilaos: Emacs: new ‘doric-tiger’ and ‘doric-lion’ for the ‘doric-themes’

I just added two new themes to my minimalist doric-themes package. doric-tiger is a light theme, while doric-lion is dark. Both use orange and yellow colours. Below are sample screenshots.

doric-tiger

doric-tiger theme sample

doric-tiger theme sample

doric-tiger theme sample

doric-tiger theme sample

doric-lion

doric-lion theme sample

doric-lion theme sample

doric-lion theme sample

doric-lion theme sample

Part of development

The new themes will be available in version 1.2.0. I think they already are well defined and may not need furter changes. Though I will continue to test—and possibly tweak—them over the course of the next two weeks or so.

Sources

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.

-1:-- Emacs: new ‘doric-tiger’ and ‘doric-lion’ for the ‘doric-themes’ (Post Protesilaos)--L0--C0--2026-06-28T00:00:00.000Z

Jakub Nowak: New Package: Gomuks.el

For the past few months I've been on and off working on a new package for Emacs, which is my first attempt at a proper major mode "application".

It is a wrapper/frontend for Gomuks, which is a Matrix client written in Go. Typically, Gomuks runs a web app client for Matrix, which connects to the Gomuks backend via a websocket. My wrapper, gomuks.el, connects to the websocket and acts as a full (well, rudimentary) Matrix client inside Emacs.

Why make this?

Matrix/Element is the single chat platform I use, and not having it be in Emacs has been a pain for ages, especially because the official Element client keeps getting worse (subjectively).

I would rather use a client from the terminal, and previously Gomuks was a solely TUI app that did that for me. However, they've moved away from the old TUI to a new (and better architecture), and now the TUI breaks on home-servers for me. So it was essentially the straw the broke the camels back in terms of forcing me to fix something myself.

If you're across the wider Emacs package world, you may have heard of ement.el, and are probably wondering why I can't use it. The simple answer is that ement.el doesn't handle encryption natively, instead requiring pantalaimon. Now, I've already had mixed success with pantalaimon in the past (which is why I was using the Gomuks TUI), but the repo for it has been archived as of earlier this year so I don't think it's wise to rely on it. As a side note, I'm not actually sure why it was archived; I assume the matrix org just didn't want to maintain it anymore, which is fair enough, but I would have liked some explanation on the readme at least. Anyway, Gomuks already handles encryption, and so gomuks.el does as well.

What state is it in?

This client is VERY basic. I would say it's usable, in the sense that I will use it and add features to it as I need them, but it is far from the quality of ement.el or the myriad of IRC clients in Emacs.

Basically all I have at the moment is live state synchronisation (without handling edit/delete events) and sending messages (only textual). The GUI is horrible, because I don't know how to use EWOC so I'm not, and it smells of programmer design. If you intend to use this, expect lots of unexpected jank.

That being said, I think the code is fairly high quality and well documented, so it should be fairly easy to modify and understand.

Contributing

I'm self-hosting this on my own git server, so there's no easy fork->PR workflow, and I'm not opening public registration. Sorry.

I'm happy to accept contributions though, either in the form of git patches or snippets sent to my email at birdt_@cyan.sh.

-1:-- New Package: Gomuks.el (Post Jakub Nowak)--L0--C0--2026-06-28T00:00:00.000Z

Raymond Zeitler: Preventing Errant Navigation

I often press C-<end> or C-<next> accidentally. It's annoying and disorientating because I lose my place in the document as well as my train of thought. I think of them (a bit dramatically) as "Deadly Navigation Keys." But I discovered two valuable Emacs features to make it easy to recover from them (or prevent them) as a result.

The first valuable feature is that C-<end> (which is bound to end-of-buffer) sets the mark at the cursor location prior to setting point to the end of the buffer. (I only just realized this after seeing the message "Mark set" in the minibuffer about fifty times.)

There's an almost-applicable saying, "What goes up, must come down," except in this case I'd say, "What gets pushed, may be popped." A simple invocation of C-h a pop.+mark revealed the pop-to-mark-command. And then, happily, M-x pop-to-mark-command returned me to exactly where I left off. It was the undo feature that Emacs had been missing -- a sort of undo-cursor-motion.

Recovering from beginning-of-buffer is just as easy. It, too, pushes mark.

There are, however, other Deadly Navigation Keys that do not push mark. In fact, they don't even change the position of the cursor. Instead, they scroll the buffer. I'm referring to scroll-left and scroll-right . These are disabled by default. However, I figured that only babies needed to shielded from such commands, so I enabled them.

Holy cow was that a mistake! On my laptop keyboard, the page down and right arrow keys are crammed very close together. I often press C-<right> to advance one word, but that's only 1 cm away from C-<next>, which is bound to scroll-left. And scroll-left alters the view of the buffer such that all the content I been work on shifts off screen. The first time it happened, I freaked, of course, and then I checked the status bar to see if the buffer had been modified. (It hadn't.) So I closed Emacs entirely and restarted it. Then I re-disabled scroll-left and scroll-right.

But then it occurred to me to disable end-of-buffer and beginning-of-buffer, too, because I rarely use those functions, and if I really need to call them, all I'd have to do is press the spacebar after each C-<end>. Disabling a command is a convenient way of seeking confirmation without making the user type "yes" (or "y").

What commands do you have disabled? And why?

-1:-- Preventing Errant Navigation (Post Raymond Zeitler)--L0--C0--2026-06-27T22:17:17.917Z

Irreal: Toggling Ghostel

Randy Ridenour is living proof that you don’t have to be a young nerd to master Emacs and Elisp. Ridenour, a professor of Philosophy, has shown repeatedly that it’s possible for a nontechnical person to become sufficiently proficient with Emacs and Elisp to bend the editor to his will and make it work as he needs it to.

Irreal has recounted some examples of Ridenour’s past use of Elisp to customize his work environment and now there’s a new one. Lately he decided to try ghostel for his terminal emulator. He was already using ghostty as his macOS terminal emulator so the change made sense.

The change worked out well and Ridenour was pleased with the result except for one thing. With his previous Emacs terminal emulator, eat, he was able to toggle the emulator on and off easily but it didn’t work with ghostel. Rather than look for another package to solve this, he decided to fix it himself.

The desired functionality was

  1. If no instance of ghostel is running, start one in the current window.
  2. If there is already a ghostel instance running in another window, switch to it.
  3. If there is a ghostel instance running in the current window, bury it.

Ridenour implemented that functionality with two functions. The first gets the active buffer of any running instance of ghostel or returns nil if there is none. The second function implements the three steps above in a straightforward way. The code is in his post and easy to understand so take a look if you have a similar need.

-1:-- Toggling Ghostel (Post Irreal)--L0--C0--2026-06-27T14:25:33.000Z

Emacs APAC: Announcing Emacs Asia-Pacific (APAC) virtual meetup, Saturday, June 27, 2026

This month’s Emacs Asia-Pacific (APAC) virtual meetup is scheduled for Saturday, June 27, 2026 with BigBlueButton and #emacs on Libera Chat IRC. The timing will be 1400 to 1500 IST.

The meetup might get extended by 30 minutes if there is any talk, this page will be updated accordingly.

If you would like to give a demo or talk (maximum 20 minutes) on GNU Emacs or any variant, please contact bhavin192 on Libera Chat with your talk details:

-1:-- Announcing Emacs Asia-Pacific (APAC) virtual meetup, Saturday, June 27, 2026 (Post Emacs APAC)--L0--C0--2026-06-26T17:54:46.000Z

Meta Redux: copilot.el 0.7

Good news, everyone – copilot.el 0.7 is out! And it’s a big one.

Ever since I took over the maintenance of copilot.el in the spring of 2025, I’ve had one north star: close the gap between Emacs and the editors that enjoy first-party Copilot support. VS Code and (Neo)vim got the rich, official experience – chat, agents, the whole shebang – while we Emacs folks were stuck with humble ghost-text completion. That never sat right with me. Emacs deserves better.

For a bit of perspective on the pace: in its first three years the project never shipped a single tagged release. 0.7 is the seventh I’ve cut since taking over. We’ve been busy.

For a long time that gap felt unbridgeable. We were essentially reverse-engineering an undocumented protocol and replicating it in Elisp, always one step behind. But then Microsoft open-sourced @github/copilot-language-server, and everything changed. Suddenly we had a documented, first-party LSP server to talk to – the very same backend that powers Copilot in VS Code. The migration to it in 0.4 was the turning point. From that moment on, catching up to the other editors stopped being a fantasy and became a (long) TODO list.

We’ve been chipping away at that list ever since. A few months ago, in 0.5, we added basic support for Copilot Chat – the first time the package did anything beyond completion. 0.6 brought Next Edit Suggestions (NES) and the experimental beginnings of agent mode. And now 0.7 fills in pretty much all of the remaining blanks.

So what’s actually in it? Quite a lot. Let me walk you through the highlights.

Agent Mode, For Real

Chat is nice, but talking to an assistant that can’t do anything gets old fast. Enter agent mode:

(setopt copilot-chat-use-agent-mode t)

With this on, Copilot can run tools to actually get work done – run shell commands, create and edit files, read your diagnostics, fetch web pages. Ask it to “add a test for foo and run it” and it’ll write the test, invoke your test runner, read the output, and iterate, all without you leaving the chat buffer.

Of course, letting an AI run commands and rewrite your files unsupervised is a recipe for excitement of the wrong kind, so every tool call asks for confirmation first. File edits show you a diff preview before you approve them, and if you trust a particular tool you can pick always to stop being asked for the rest of the conversation. Long-running terminal commands now run asynchronously too, so Emacs stays responsive while that test suite chugs along – and C-g aborts a command that’s overstaying its welcome.

Workspace Awareness

An agent that can only see the current buffer isn’t much of an agent. So 0.7 teaches copilot.el to answer the server’s workspace requests: Copilot can now search your project by file glob or by text/regex (via ripgrep, so your .gitignore is respected), read files, and list directories across the whole tree. That’s what makes “where is X handled in this codebase?” actually work.

For the truly ambitious there’s also opt-in semantic search:

(setopt copilot-chat-enable-semantic-search t)

Flip that on and the server builds an embeddings index of your workspace, so you can ask whole-codebase questions instead of pointing at specific files. It’s off by default, since indexing isn’t free, but it’s there when you want it.

MCP Support

Model Context Protocol is how you give an assistant extra superpowers, and copilot.el now speaks it. Point copilot-mcp-servers at the servers you want and their tools show up in chat, right alongside the built-in ones:

(setopt copilot-mcp-servers
        '(:fetch (:command "uvx" :args ["mcp-server-fetch"])
          :memory (:command "npx"
                   :args ["-y" "@modelcontextprotocol/server-memory"])))

M-x copilot-chat-list-mcp-tools shows you which servers connected and what they offer (and complains loudly when one fails to start, which beats the old silent treatment).

A Chat You Can Actually Use

The chat buffer grew up too. A few of the quality-of-life additions:

  • Slash commands (C-c /) – /explain, /fix, /tests, /doc, and friends, fetched live from the server.
  • Acting on code blocksC-c C-i inserts the code block at point into your source buffer, C-c M-w copies it to the kill ring. No more copy-paste gymnastics out of a read-only buffer.
  • Attaching contextcopilot-chat-add-file-reference (C-c C-f) and copilot-chat-add-region-reference let you hand Copilot specific files or selections along with your question.

Odds and Ends

A grab bag of smaller, but handy, additions:

  • M-x copilot-quota shows how much of your chat, completion, and premium-request allowance is left – useful now that the fancy models eat into a quota.
  • Copilot will tell you when a suggestion closely matches public code and collect those matches, with their licenses and reference URLs, in a buffer you can open via M-x copilot-list-code-citations.
  • A pile of bug fixes, including agent-mode tool confirmations that were flat-out broken against newer servers, and chat finally picking a sensible default model on its own.

As always, see the full changelog for the complete rundown.

Epilogue

Agent mode, MCP, and semantic search are all opt-in, so nothing here changes your setup unless you ask for it. But I’d love for you to ask for it! Turn on agent mode, point it at a real task, and tell me where it shines and where it falls on its face. This kind of feature only gets good with people kicking the tires and filing detailed reports, so please do open issues and let me know how it goes. Your feedback is what shapes the next release.

I’d be remiss not to acknowledge it: there’s a decent chance that by now half of you have wandered off to Claude Code or some other agentic CLI, and that pouring this much time and energy into Copilot support for Emacs is, objectively, a questionable use of a human life. Fair enough.

But here’s the thing – Emacs has always excelled at providing amazing support for niche technologies long after the rest of the world has moved on. We’re the people who’ll lovingly maintain a mode for a language nobody has written any code in since 2003. Keeping Copilot first-class in Emacs is squarely in that grand tradition, and I, for one, intend to see it through. :-)

That’s all I have for you today. Keep hacking!

-1:-- copilot.el 0.7 (Post Meta Redux)--L0--C0--2026-06-26T09:00:00.000Z

Nicolas Cavigneaux: Not Quite Vim: Switching to Evil in Doom Emacs

Introduction

I’ve used Emacs for many years.

My fingers knew the native bindings by heart. C-a, C-e, C-k, M-f, the whole choreography of chords. I never really questioned it.

Then I moved to Doom Emacs.

Doom is built around Evil, the Vim emulation layer. Vim keybindings are the default, on purpose. And the first thing I did was turn them off.

It was pure habit. I had years of native muscle memory and no intention of throwing it away to relearn how to move a cursor.

So for a while I ran Doom the “wrong” way: a Vim-first distribution with Vim disabled.

Later, I changed my mind.

Giving Evil a Real Chance

At some point it felt silly to run Doom against its own grain.

The whole project is designed around Evil. Most of its polish, its defaults, its bindings assume you’re editing the Vim way. By keeping native bindings I was fighting the tool instead of using it.

So I flipped the switch and committed to it.

The beginning was rough.

There’s a whole grammar to learn. Modes. Operators. Motions. Text objects. A language where pressing keys in the wrong order does nothing useful, and where the “obvious” Emacs reflex is suddenly wrong.

For a few days I was slower at everything.

Then, slowly, it started to make sense. And once it did, I kept it. This is still how I edit today.

One thing matters before the bindings, though.

What I switched to isn’t really Evil. It’s Doom in Evil mode, which is a very different, much friendlier thing.

It’s Doom, Not Raw Vim

This distinction took me a while to appreciate.

Evil on its own is the engine: it teaches Emacs to speak Vim. But raw Evil is spartan. Doom wraps it in a coherent set of defaults, a leader key, discoverable menus, and dozens of small integrations that make the modal model pleasant instead of merely functional.

which-key is the clearest example. Start a key sequence, pause, and Doom shows you every possible continuation in a popup. You’re never stranded wondering what comes next.

So when I say I switched to Evil, I really mean I switched to Doom’s curated take on it. I don’t think I would have stuck with bare Evil. The polish is what kept me.

The Mental Model That Made It Click

The breakthrough wasn’t memorising bindings.

It was understanding that Vim editing is a small language: a verb, then something to act on.

You combine an operator (the verb) with a motion or a text object (what to act on):

  • d — delete
  • c — change (delete, then drop into insert mode)
  • y — yank (copy)

And the things to act on:

  • w — to the next word
  • iw / aw — “inner word” / “a word” (with surrounding space)
  • i) / a) — inside / around parentheses
  • ip / ap — inside / around a paragraph

Once that clicks, you stop memorising commands and start composing them. dw, ci), yap — none of these needed to be learned individually. They fall out of the grammar.

The other revelation was the dot command.

Pressing . repeats your last change. Edit one word, jump to the next occurrence, press ., and you’ve made the same edit again. It sounds trivial. It’s one of the things I’d miss the most.

The Editing Bindings I Reach For Daily

These are the ones that earned a permanent place in my hands.

Acting on the word under the cursor:

  • daw — delete a word (and its surrounding space)
  • caw — change a word (delete it and land in insert mode)
  • diw — delete the inner word, leaving the spaces
  • ciw — change the whole inner word
  • cw — change from the cursor to the end of the word

Working with indentation, which is wonderful in indentation-heavy languages:

  • dii — delete everything at the current indentation level
  • cii — change that whole block

Doom ships the indentation text object (ii / ai), so these compose just like the word objects.

Commenting:

  • gcc — toggle the comment on the current line
  • gc + a motion — comment a region, e.g. gcap to comment a paragraph

And for shuffling things around, there’s a surprising amount you can do with operators and the p (paste) register. The Vim wiki has a good reference on swapping characters, words and lines that I still go back to.

Getting Around

Motion is where the modal model really pays off, because once you’re in normal mode the whole keyboard is navigation.

Jumping to a line is just the line number followed by G:

30G

That puts the cursor on line 30. (:30 followed by Enter does the same.)

Within a line, f and t are indispensable:

  • f( — jump forward to the next (
  • t( — jump just before it
  • ; and , — repeat that jump forward and backward

But the binding that genuinely changed how I move is Doom’s avy integration, hiding under the gs prefix:

  • gss — type a couple of characters and jump to anything visible on screen
  • gsj / gsk — jump visually to a line below / above
  • gs SPC — jump to any character, anywhere in the buffer

After a while, reaching for the mouse or hammering arrow keys feels slow.

For code, Doom’s LSP layer adds the usual suspects:

  • gd — go to definition
  • gr — find references
  • K — show documentation for the symbol under the cursor

Doom’s Polish: The Leader Key

This is the part that has very little to do with Vim, and everything to do with why Doom is comfortable.

Almost every non-editing action lives under the SPC leader key:

  • SPC f f — find a file
  • SPC b b — switch buffer
  • SPC p p — switch project
  • SPC w — window management
  • SPC s — search (in buffer, project, etc.)
  • SPC g g — open Magit
  • SPC : — run a command (the old M-x)

Because of which-key, I never had to memorise these up front. I’d press SPC, read the menu, and drill down. The bindings stuck on their own through repetition.

And when I want to know what a key does in the current mode, the bindings help is right there under SPC h b — a searchable list of everything currently bound. It’s how I discovered half of what I now use.

What I Didn’t Fully Convert

I’m not a purist about any of this.

A few Emacs reflexes survived the switch, simply because my fingers refused to let them go.

The big one is C-g. It’s my universal “get me out of here”, and it works everywhere in Doom, so I never replaced it. Leaving insert mode is the clearest case: I still do it three different ways depending on where my hands are — Esc, jk, or C-g. I never forced myself to settle on one.

In insert mode I also kept C-a and C-e to jump to the start and end of the line. That’s an Emacs-ism rather than a Vim one — Doom keeps those motions alive in insert state — and it’s so wired into my hands that switching to a normal-mode motion just to reach the end of a line never felt worth it.

And every so often I still reach for M-x instead of SPC : to run a command. Old habits.

On top of that I keep a handful of small custom bindings, and they fall into two telling camps.

Some of them duplicate what Evil already does. I bound my own commands to duplicate a line and to change the case of a word, even though Evil has perfectly good answers — yyp to duplicate a line, gUiw / guiw to upper- or lower-case a word. I kept the Emacs versions purely out of reflex.

Others have no Vim equivalent at all, and those I would never give up. Narrowing is the clearest example: focusing the buffer on a single region or function is a pure Emacs power, so I gave it a proper home under the leader.

(map! :leader
      :desc "Narrow to region" "n r" #'narrow-to-region
      :desc "Narrow to defun"  "n f" #'narrow-to-defun
      :desc "Widen"            "n w" #'widen)

That’s the honest part of the story. I didn’t replace one orthodoxy with another. I took Doom’s Evil as a strong default and bent the last few percent to fit my hands.

If anything, that’s the lesson: the goal isn’t to edit like Vim, or like Emacs. It’s to edit like you, on top of sane defaults.

Resources

A few things that helped me far more than any single cheat sheet:

Was It Worth It?

Yes — with an asterisk.

It was worth it because, after the rough first days, modal editing genuinely made me faster: the verb-plus-object grammar turns most common edits into a couple of deliberate keystrokes.

The asterisk is that “switching to Evil” undersells what actually happened. I didn’t move to Vim. I moved to Doom, resisted its way of working for a while, then finally let it do what it was built to do.

The grammar fought me. Then it made sense. And it stuck.

-1:-- Not Quite Vim: Switching to Evil in Doom Emacs (Post Nicolas Cavigneaux)--L0--C0--2026-06-25T22:00:00.000Z

Irreal: Simulating The Unix at With Emacs

Charles Choi has a nice post on simulating the Unix at command with Emacs. Back in the old days, Choi used to use the Unix at command to notify himself of various events. In macOS, the at command is disabled by default because its use can deplete the battery. Old habits die hard though, and Choi wanted a way to get the same behavior.

He started with the Emacs run-at-time function that does roughly the same thing as the at command except it runs an Emacs function instead of a command line function. Choi filled that gap with a macOS shortcut that he calls nota. The idea is that a small Emacs function schedules nota with the desired message at whatever time you want.

If you’d like to use his workflow, his post has a link to his shortcut that you can download. Of course, the shortcut bit works only with macOS but I doubt it would be hard to adapt to any Unix-like system. As far as I can see, the shortcut mechanism is merely a convenience and could probably be replaced with something like the Unix message command.

Regardless, if you’re looking for a handy way to serve a notification at a certain time and, like Choi and me, you have Emacs running all the time, take a look at his post.

-1:-- Simulating The Unix at With Emacs (Post Irreal)--L0--C0--2026-06-25T14:41:30.000Z

Jakub Nowak: org-roam Orphans

I use org-roam for taking notes, but I also like to have indexes (just org roam nodes that link their contents) that link everything together. This means that if I want nodes properly linked when I make them, I first have to find the index they belong to insert them into, which really ruins the flow. I would rather go through the graph regularly and clean things up later, but aside from squinting at org-roam-ui finding those orphan nodes is actually quite annoying from within Emacs.

Thankfully I stumbled upon this note by Justin Abrahms, which gives a nice SQL query for finding orphans, and I've written a bit of Elisp around it to prompt for the resulting nodes with completing read. Hopefully someone else finds this useful!

(defun org-roam-find-orphan ()
  "Find all orphaned nodes and then provide their paths to open with completing-read."
  (interactive)
  (let* ((org-roam--db (sqlite-open org-roam-db-location))
       (org-roam-orphans
        (sqlite-select
         org-roam--db
         "select * from nodes n
where not exists ( select null from links l where n.id = l.source )
and not exists ( select null from links l where n.id = l.dest );")))
    (sqlite-close org-roam--db)
    (find-file
     (completing-read
      "Orphan node: "
      (mapcar
       (lambda (rec)
       (string-replace "\"" "" (cadr rec)))
       org-roam-orphans)))))
-1:-- org-roam Orphans (Post Jakub Nowak)--L0--C0--2026-06-25T00:00:00.000Z

Charles Choi: Scheduling Future Tasks in Emacs

Back when I had to run a lot of long-running simulations on Unix systems, I became an ardent user of the at command, which lets one schedule a program to run at a future date. I also used at to schedule different notifications, like sending a message to a pager (yer, how quaint).

Moving to macOS (nee OS X) changed that, as it disabled by default the at command due to battery life management. As I was unwilling to give up battery life for at, I learned to live without it. That was, until I learned that Emacs had run-at-time.

The run-at-time command does as it says on the tin: it will run a function at some future specification of time.

1
(run-at-time TIME REPEAT FUNCTION &rest ARGS)

That specification of time is “natural language-like” in that multiple string representations of time can be accepted, both absolute and relative.

The function can be anything Elisp offers, but running shell commands is what makes run-at-time a substitute for at, with some caveats.

Jobs that are submitted by run-at-time can be listed out via the list-timers command. You can cancel a job (actually a timer object) using the c key, bound to timer-list-cancel. Note that Emacs disables list-timers by default as it exposes timers that you should likely not mess with. Be forewarned.

Another caveat is that using run-at-time presumes that your Emacs session is still running at that future time. If you have Emacs running all the time (particularly as a server) then this should work without complication.

Example: Scheduling a macOS Notification

A common use I have for run-at-time is to send myself a notification at a future time. The way I prefer to be notified is using the macOS Notification Center. Here’s an example of what a notification looks like:

The above notification was sent using a macOS Shortcut I’ve defined named nota. Shown below is how nota is defined in the Shortcuts app.

A macOS shortcut can be invoked from the command line. We can wrap this capability in the Elisp function cc/run-nota.

1
2
3
4
5
6
7
8
(defun cc/run-nota (msg)
  "Run nota shortcut with MSG."
  (process-lines
   "shortcuts"
   "run"
   "nota"
   "-i"
   msg))

Finally we can schedule cc/run-nota with run-at-time through the wrapper command cc/notify-at.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(defun cc/notify-at (&optional start-time msg)
  "Send a nota notification with MSG at START-TIME.

This command invokes `cc/run-nota' with MSG at START-TIME passed into
`run-at-time'."
  (interactive)
  (let* ((start-time (if start-time
                         start-time
                       (read-string "Time: ")))
         (msg (if msg
                  msg
                (read-string "Message: "))))
    (run-at-time start-time
                 nil
                 #'cc/run-nota
                 msg)

    (message "Show notification “%s” in/at %s" msg start-time)))

Users wishing to install the nota macOS shortcut can download it via the following iCloud link:

nota shortcut (via iCloud)

Closing Thoughts

run-at-time is an underappreciated built-in which has given me much of what I used to use at for. If you use Emacs for implementing any kind of automation, you should include run-at-time as part of your toolbox for it.

-1:-- Scheduling Future Tasks in Emacs (Post Charles Choi)--L0--C0--2026-06-24T19:25:00.000Z

Charlie Holland: svg-margin: Better Gutters for Emacs

1. TLDR

Emacs can draw line-level indicators in the built-in fringe and margin, the thin gutters beside the buffer text. The fringe gives you a single monochrome mark per side, and the margin can technically hold more, but getting several independent sources to share one line cleanly is surprisingly hard to pull off in the plain text rendering that the margin uses out-of-the-box.

svg-margin (code on GitHub) fixes this by rendering indicators as SVG in the window margins, so any number of independent "providers" present text, icon glyphs, and clickable markers side by side on one line. Like my svg-line package, it overlays SVGs onto built-in UI components (the margin in this case) by leveraging Emacs's built-in SVG support. My personal config channels many sources to it, including VC, flycheck, evil marks, Org elements, whitespace indicators, and live symbol occurrences.

svg-margin-annotated.png

Figure 1: See svg-margin in action (to the left of the grey fringe). Note the lines where there are multiple indicators. On line 38, we have an evil mark (m), a symbol-overlay indicator ({}), and git-gutter. I'm also using the right margin to display a white-space indicator. The margin grows and shrinks to accommodate an arbitrary number of indicators.

2. About

I lean on the fringe and margin heavily because I'm a HUD maximalist, and scanning a buffer's gutter at a glance is almost always faster than parsing the same data out of the buffer's text.

To satisfy my greed for HUD, I yearned for a flexible gutter where any number of independent (that's a key word, remember it) indicator sources can coexist without competition on a given line, and without me having to allocate them to predetermined slots. I want an API that's like "here's a gutter, any package can push pretty indicators to it".

Emacs's fringe can't give me that because it's one monochrome bitmap per side. It's charming in its ugliness, and very useful (arguably necessary) for things like line wrap indicators. It's the Quasimodo of Emacs's UI components: rough around the edges, naive, and loyal.

Emacs's margin is the Esmeralda to the fringe's Quasimodo, and provides a much better stage for the decoupled-indicator-provider vibe I'm going for, mainly because it supports multiple indicators on a given line. But it falls ever so short of my requirements because it lacks a way to compose multiple indicators together.

The main contribution of svg-margin is exactly that missing compositor. It lets you push however many indicators you want into a given margin line, with graceful resizing (over the built-in truncation), and with position priority (for example, so that git-gutter information always renders closest to the buffer's text).

To address an anticipated question: Why use SVG when you can use a compositor like the one defined in svg-margin with the built-in margin? Basically, it works, but it's very difficult to get the built-in margin's text rendering to play nice with pretty glyphs in the multiple-indicator-per-line scenario. As optional reading, you can read more about why in the Why an image, not text section below.

3. svg-margin's Features

  • Multi-column packing: several indicators stack side by side on one line, in both margins; the margin grows and shrinks to fit, with no upper bound on the number of indicators per side.
  • Decoupled providers: a provider of indicators is a simple function of the buffer returning a list of indicators, so independent packages coexist without conflict.
  • Rich indicator style: built-in shapes (dot, ring, bar, box, triangle), centred text, Nerd Font icon glyphs, or a custom :draw function can all be used to render indicators in the margin.
  • Clickable anything: any indicator can carry a left-click action, a right-click menu, and hover help with a highlight.
  • No jitter on buffer switch, text-scale aware: margin widths are buffer-local and indicators resize with text-scale. This avoids jitter when switching buffers and ugly or truncated repaints, respectively.

4. Configuration

A provider is a function of the buffer that returns a list of indicator plists. As shown below, you simply define the provider, register it with svg-margin-register-provider and then enable svg-margin-mode (or global-svg-margin-mode).

4.1. A simple provider

This example puts a red dot beside every line containing the string TODO:

(svg-margin-register-provider 'todo
  (lambda (_buffer)
    (let (out) ;; out is shorthand for 'output'
      (save-excursion
        (goto-char (point-min))
        (while (re-search-forward "\\_<TODO\\_>" nil t)
          (push (list :line (line-number-at-pos) :shape 'dot :color "#cc3333")
                out)))
      out)))

(svg-margin-mode 1)            ; or (global-svg-margin-mode 1)

The provider (that lambda) returns a list of indicator plists, each with a position (:line or :pos), a :shape, and a :color. svg-margin-mode enables the gutter in this buffer.

4.2. Decoupled providers

The real point of providers in svg-margin is that they are decoupled (or independent). In this example, three separate ones (a VC bar, a lint icon, and an evil-style mark) are registered. Two of them happen to target the same line, and svg-margin gracefully handles packing them into adjacent columns within the margin:

;; A VC provider: a coloured bar.  Highest priority, so it sits nearest the text.
(svg-margin-register-provider 'vc
  (lambda (_buffer)
    (list (list :line 10 :shape 'bar :color "#8fb39a" :help "added")))
  :side 'right :priority 9)

;; An independent lint provider.  It knows nothing about 'vc, but because it
;; emits an indicator for the same line, svg-margin packs it into the next
;; column over.  This one is a clickable icon with an action and a menu.
(svg-margin-register-provider 'lint
  (lambda (_buffer)
    (list (list :line 10
                :text (nerd-icons-codicon "nf-cod-bug") :font "Symbols Nerd Font Mono"
                :color "#cf9999"
                :help "syntax error"
                :action #'flycheck-list-errors :action-help "list errors"
                :menu '(("Next error" . flycheck-next-error)
                        ("Explain"    . flycheck-explain-error-at-point)))))
  :side 'right :priority 5)

;; A third provider, on the LEFT margin: a letter mark coloured by a face.
(svg-margin-register-provider 'marks
  (lambda (_buffer)
    (list (list :line 25 :text "a" :face 'warning)))
  :side 'left)

(global-svg-margin-mode 1)
  • The key take-away here is that three independent providers (vc, lint, and marks) never reference each other. vc and lint both emit for line 10, so svg-margin packs them into adjacent columns of one image, ordered by :priority (vc with priority 9 is rendered nearest the text, and lint with priority 5 is rendered next outward).
  • :side / :priority passed to register-provider are defaults: they add these keys for any indicator that doesn't set them.
  • :text + :font: a string drawn centred. Note that when you use a Nerd Font, the text can be a glyph. :face uses a face's foreground instead of a literal :color.
  • The :action (left-click action) / :action-help (hover-over) / :menu (right-click context menu) keys turn an indicator into a button.

4.3. Reclaiming the fringe

Because a provider just reads data, you can move what a package draws in the fringe into the margin (with one practical exception). For example, here are evil's marks, which display in the fringe by default, mirrored into the left margin:

(setq svg-margin-disable-fringe 'left)   ; reclaim the left fringe

(svg-margin-register-provider 'evil-marks
  (lambda (buffer)
    (with-current-buffer buffer
      (cl-loop for (ch . m) in (bound-and-true-p evil-markers-alist)
               when (markerp m)
               collect (list :pos (marker-position m)
                             :text (char-to-string ch)
                             :face 'font-lock-keyword-face))))
  :side 'left)

The provider reads evil-markers-alist and renders each mark's letter at its position.

svg-margin-disable-fringe tells svg-margin to collapse the named fringe (left, right, or both) to zero on each render, so the margin effectively reclaims the evil marks from the fringe's space. This is also how my config moves git-gutter, flycheck, and evil marks off the fringe into the better gutter provided by svg-margin.

5. Why an image, not text

This is optional reading for those curious as to why SVG is justified in this case.

Packing several independent indicators into one built-in margin line has two challenges, one compositional (already solved by the compositor), and the other graphical.

With the built-in margin displaying multiple indicators per line, I found that pixel-exact layout in text is a losing game. In the built-in margin, indicators are laid out as characters. The margin is reserved in whole character columns, but text glyph advances (e.g. for Nerd Font icons) don't divide evenly into them, and I found that this became worse at fractional text-scale. The challenges that arose for me from this char advance inconsistency:

  • Text clipping: the rendered content runs slightly past the reserved width and the outermost marker is cut off at the margin's edge.
  • Horizontal jitter: glyph advances vary, so when one indicator appears or disappears, its neighbours shift by a sub-pixel.
  • Line-height growth: Nerd-Font icons often raster taller than the text line and stretch it, and trying to trim their height to fit shrinks their character advance below a character, which reintroduces the jitter issue.

I'm sure it was possible to get around this, but I had to add enough code to account for these issues that the simple compositor-only implementation quickly outgrew the compositor-plus-SVG implementation.

The reason SVGs are easier to work with is that an image has an exact, author-controlled pixel width. I found there were no issues handling character advances, no clipping, no jitter, no line-height problems, and that the SVG-based approach worked at any text-scale.

6. Caveat: line-wrap indicators stay in the fringe

One thing you can't pull into the margin is the line-wrap (continuation) and visual-line arrows. I tried this for a while, and to my surprise, I couldn't get it to work.

The reason is that those arrows are drawn by Emacs's redisplay engine per screen row as it lays the buffer out. When a long line spans several screen rows, each wrapped row gets one line-wrap indicator. Margin content, by contrast, is anchored to a buffer position, and renders on the single screen row that contains that position.

The points where a line wraps are not buffer positions, and I found that they're actually decided at display time from the window's width and font. So with the margin, there is nothing to anchor a marker to on the second visual row, short of recomputing the line-breaking yourself (reimplementing part of redisplay), which I don't think is worth the squeeze. After all, the fringe already does this well.

7. Note on Inspiration

svg-margin is the gutter sibling of svg-line. Both grew out of the same realization that Emacs's native SVG support, which Nicolas Rougier's explorations showed could stand in for so much of the display engine, applies just as well to the margins as to the status bars. Where svg-line rebuilt the *-lines, svg-margin rebuilds the gutter.

-1:-- svg-margin: Better Gutters for Emacs (Post Charlie Holland)--L0--C0--2026-06-24T11:22:06.000Z

Bozhidar Batsov: Neocaml 0.9: A Better REPL, Dune/Opam Completion, and More Robustness

It’s been a couple of months since the last neocaml release, and the reason is simple — for a while there I was genuinely out of ideas. Back when I shipped 0.6 I declared (again!) that I was done with new features, and this time I almost meant it. But ideas have a way of creeping back in, and 0.9 turned out to be a meaty release. Here are the highlights.

A much nicer REPL experience

The biggest chunk of work went into the REPL (toplevel) integration. I’m well aware that the OCaml toplevel isn’t terribly popular with seasoned OCaml developers — most of them reach for a proper build and a debugger instead. But I think newcomers get a lot of mileage out of a REPL, and (no surprise to anyone who’s followed my work) I’m a Lisper at heart with a real soft spot for interactive development. Clojure and Emacs Lisp spoiled me, and I want OCaml beginners to taste a bit of that too.

So, what’s new:

  • A dedicated REPL per project. The REPL buffer is now named after its project (e.g. *OCaml: myproject*), and the send commands route to the current buffer’s project REPL. You can have several projects running side by side without them stepping on each other.
  • Choose your toplevel. The new neocaml-repl-flavor lets you pick between ocaml, utop, and dune-utop. Set it globally, or per project via .dir-locals.el:

    1
    
    ((neocaml-mode . ((neocaml-repl-flavor . dune-utop))))
    

    The active flavor shows up in the REPL’s mode line, so you always know what you’re talking to.

  • Send a phrase and step. C-c C-n (neocaml-repl-send-phrase-and-step) sends the phrase at point to the REPL and moves on to the next one — perfect for walking through a file top to bottom while you experiment.
  • #require from Emacs. neocaml-repl-require loads a findlib package into the running toplevel without you having to type the directive by hand.
  • Restart on demand. neocaml-repl-restart kills and restarts the toplevel when things get into a weird state.

Completion for Opam and Dune, no LSP required

This one I’m particularly happy with. .ml/.mli files have ocaml-lsp-server to lean on for completion, but the auxiliary file formats have no language server at all. That’s exactly the kind of gap neocaml is meant to fill, so both neocaml-dune-mode and neocaml-opam-mode now ship a completion-at-point backend.

In a dune file you get completion for stanza names, the field names valid for the enclosing stanza, and library names inside libraries/pps fields:

(library
 (name my_lib)
 (libraries str re|))   ; <- completes both your own libraries and installed findlib ones

The library candidates combine your project’s own libraries with whatever’s installed in the active Opam switch. And it’s switch-aware — if there’s a project-local switch (an _opam/ directory), neocaml detects it and queries it via opam exec --, without any configuration on your part. The results are cached per project, so it stays snappy.

In opam files you get completion for field and section names, and package names inside depends/depopts/conflicts (sourced from opam list):

depends: [
  "dune" {>= "3.0"}
  "cmd|"   ; <- completes to cmdliner and friends
]

Both backends can be toggled off via neocaml-dune-complete-libraries and neocaml-opam-complete-packages if you’d rather not have them.

Robustness improvements

A good chunk of this release is the unglamorous but important work of making things just behave correctly:

  • Character literals and quoted strings at the syntactic layer. Tree-sitter fontifies '"', '(', and {|raw "strings"|} correctly, but the syntax table underneath was getting confused — which broke forward-sexp, delete-pair, and electric-pair-mode around those constructs. The fix was a good old syntax-propertize-function. I wrote up the whole story over on Emacs Redux in Tree-sitter Modes Still Need a Syntax Table, if you’re into mode-writing internals.
  • project.el integration. A directory with a dune-project file is now recognized as a project root (even without version control), _build/ and _opam/ are ignored, and compile-command defaults to dune build in dune projects.

There’s more in there too — ocamlformat integration (C-c C-f), clickable URLs and bug references in comments, a font-lock level selector, and richer menus across the modes.

OCaml 5.5 support, and the ABI 14 balancing act

OCaml 5.5 was released on June 19th, so this felt like a good moment to ship 5.5 support in neocaml. The ocaml and ocaml-interface grammars now track tree-sitter-ocaml v0.25.0, which brings the 5.5 grammar along with it.

There’s a wrinkle here that’s worth explaining, because it’s shaped the last few releases. A tree-sitter grammar gets compiled into a parser that speaks a particular ABI version, and Emacs can only load parsers up to the latest ABI supported by the libtree-sitter it was built against — it’s not the Emacs version itself that sets the ceiling. In practice a lot of Emacs 30 builds out there (notably Homebrew’s on macOS) are linked against tree-sitter 0.24, which tops out at ABI 14; you need an Emacs built against tree-sitter 0.25+ to load ABI 15 grammars. The trouble is that the tree-sitter 0.25 CLI now generates ABI 15 parsers by default, so any grammar regenerated with current tooling produces something those builds simply can’t load — you install it and it just errors out. Emacs 31 will ship with newer tree-sitter and make ABI 15 the common case, but it’s not out yet. (This isn’t a neocaml problem as such; it’s been biting tree-sitter modes across the ecosystem.)

After a few users ran into exactly this, I’ve made a deliberate decision: stick to ABI 14 grammars until Emacs 31 is widely available. That effort started a couple of releases back — in 0.8.1 I lowered the ABI requirement from 15 to 14 across the opam, dune, and ocamllex modes, switched the menhir recipe to tmcgilchrist/tree-sitter-menhir, and pinned ocamllex back to v0.24.0, all of which target ABI 14 (#42). 0.9 extends that policy to the core OCaml grammars.

The catch with v0.25.0 is precisely that it generates an ABI 15 parser. Happily, the 5.5 grammar didn’t actually need any ABI 15 features — the bump rode along with the CLI upgrade — so an ABI 14 regeneration of the very same grammar is a drop-in. Big thanks to 314eter, the tree-sitter-ocaml maintainer, for cutting a v0.25.0-abi14 tag for exactly this purpose (#141). The one snag was that tagging normally triggers releases to NPM, crates.io, and PyPI, so I sent a small PR to skip publishing for ABI-suffixed tags (#142), and the tag followed. neocaml now pins both grammars to it.

Roadmap and docs

If you’re curious where neocaml is headed, I’ve started keeping a ROADMAP.md with ideas and guiding principles (short version: tree-sitter first, lean on the LSP stack for .ml/.mli, and own the auxiliary modes that have no language server). The project also has a proper documentation site now at neocaml.org, so there’s a real home for the details beyond the README.

Give it a Try

As always — update from MELPA, play with it, and let me know how it goes. The full list of changes is in the 0.9.0 release notes. Bug reports, feature requests, and pull requests are all welcome on GitHub.

That’s all from me, folks! Keep hacking!

-1:-- Neocaml 0.9: A Better REPL, Dune/Opam Completion, and More Robustness (Post Bozhidar Batsov)--L0--C0--2026-06-24T07:00:00.000Z

Raymond Zeitler: Link diary Event to Org Heading

Suppose you want to schedule a recurring meeting in the diary, yet you want to access an org heading for note-taking and clocking time. You can add a link to that org heading in the description of the diary event.

Here's how I'd do it:

  • Place point on the desired Org heading
  • Invoke org-store-link, which might be bound to C-c l
  • In the diary file...
    • Add the date expression for the corresponding event. In this example, we'll schedule an event that occurs from 7pm to 8pm on the third Tuesday of every month.
      • %%(diary-float t 2 3) 19:00-20:00
    • Add a space and then the link as the event description with M-x org-insert-link
      • Note that C-c C-l might not bound to org-insert-link in diary-mode
  • That's it!
-1:-- Link diary Event to Org Heading (Post Raymond Zeitler)--L0--C0--2026-06-23T19:38:40.369Z

Randy Ridenour: Toggle Ghostel

I have been using Eat and toggle-term.el for terminal emulation in Emacs. After seeing some recommendations for Ghostel, I decided to give it a try, especially since I use Ghostty as my terminal emulator on the Mac anyway. All the reports were correct, Ghostel works like a charm, but I didn’t have as smooth a toggle experience as I had with Eat. Before switching back, I wondered if I really needed to install a package just to toggle the Ghostel buffer. It turns out that writing a simple toggle function was fairly easy.

The first function returns the name of the ghostel buffer if there is one. The second function starts ghostel if there is no existing ghostel buffer, switches to the buffer if there is one, and buries the ghostel buffer if it is currently the active buffer. I had to use bind-key* to override the ghostel key-map.

(defun rlr/ghostel-buffer ()
  "Return the active ghostel buffer, or nil if none exists."
  (seq-find (lambda (buf)
              (string-match-p "\\*ghostel:" (buffer-name buf)))
            (buffer-list)))

(defun rlr/ghostel-toggle ()
  (interactive)
  (let ((buf (rlr/ghostel-buffer)))
    (cond
     ((not buf)
      (ghostel))
     ((eq (current-buffer) buf)
      (bury-buffer))
     (t
      (switch-to-buffer buf)))))

(bind-key* "<f2>" 'rlr/ghostel-toggle)

Tagged: Emacs

-1:-- Toggle Ghostel (Post Randy Ridenour)--L0--C0--2026-06-23T15:36:00.000Z

Irreal: YouTube Radio Player

As you all know by now, I’m not a Google fan and generally avoid using their products. The one exception is YouTube for videos. Apparently, YouTube also has a radio channel, which is a sort of music streaming service. I listen to my music offline so I’m not interested in steaming services and even if I were, I’d probably use Apple’s or maybe the Amazon service that I get for free as an Amazon Prime member.

Still, lots of people don’t share my antipathy to Google and at least occasionally like to stream music. One such person is Álvaro Ramírez who sometimes has the urge to listen to YouTube radio channels and, being Ramírez, built an Emacs player, ytr, to do that. As usual, he built ytr to scratch and itch and is using it to experiment with its UI with the long term goal of possibly integrating it into his music player, ready-player.

Ytr is still at an early stage of development and will probably change as Ramírez gets more experience with it. It’s not yet in MELPA, of course, but you can get a copy from its GitHub repository where you’ll find installation instructions as well as a command summary.

If you have any interest in playing YouTube radio from within Emacs, take a look at Ramírez’s post and the ytr GitHub repository.

Update [2026-06-24 Wed 10:08]: Álvaro Ramírez reached out to me offline with a clarification. What ytr actually does is download the audio track from any YouTube video. As Ramírez says, often the video portion is secondary to the audio. I’ve noticed that that’s especially true for older recordings where the video portion may consist simply of a picture of the album cover or the artist.

-1:-- YouTube Radio Player (Post Irreal)--L0--C0--2026-06-23T15:09:57.000Z

Andros Fenollosa: How I built a GPU backend for Emacs

A few months ago I became obsessed with a silly question: why does my Emacs, on a laptop with a perfectly capable GPU, draw all of its text using the CPU? And that led to others: why can't I play a video inside a buffer? Why can't I have animated cursor effects? Why can't I cross-fade between buffers? I needed to satisfy my curiosity, so I started digging.

I started reading the code, with an AI as my companion. I discovered that every glyph, every underline, every scroll is recomputed and repainted by the processor. Emacs's redisplay engine (xdisp.c) was born in an era when there was no other option, and it is tuned to the millimeter for exactly that. And nobody had managed to slip a GPU underneath without rewriting half of Emacs... until recently.

So I decided to try. What began as a weekend experiment ended up being a complete display backend for macOS with Metal, a second backend for GNU/Linux with OpenGL, a video player inside the buffer, shader-based cursor effects, and a debate of more than a hundred messages on the Emacs developers' mailing list that ranged from cairo's performance to software freedom and the ethics of artificial intelligence.

This article exists because I feel like telling the story, and it might be useful for future implementations. At the end I leave the lessons I take away and a conclusion that is not the one I expected when I started.

A note of honesty up front: I built this project with the help of an LLM as a copilot, from start to finish. I say it here just as I said it in public when I was asked. I will come back to it, because it turned out to be the most important plot twist of the whole journey.

Phase 1: the architecture decision

Anyone's first instinct would be to open the macOS code, the Cocoa backend (nsterm.m), and start replacing CoreGraphics calls with Metal calls. It is the most direct path. And it is exactly what I decided not to do.

The problem with that approach is that it ties you to one platform. If I write "Emacs with Metal", I have an Emacs for Mac and nothing else. I needed to write a display-backend abstraction that would let me have one driver per platform. So I sketched a three-layer architecture on a Post-it:

flowchart TD
    X["Redisplay engine
(xdisp.c, untouched)"]:::core --> P["src/gfxterm.c
Neutral drawing policy (plain C)"]:::policy P --> D["src/gfxdrv.h
Driver interface (~25 operations)"]:::iface D --> M["src/mtlterm.m (macOS)
Metal driver"]:::mtl D --> G["src/glterm.c (GNU/Linux, X11)
OpenGL ES / EGL driver"]:::gl classDef core fill:#37474F,stroke:#263238,stroke-width:2px,color:#fff classDef policy fill:#00897B,stroke:#00695C,stroke-width:2px,color:#fff classDef iface fill:#7CB342,stroke:#558B2F,stroke-width:2px,color:#fff classDef mtl fill:#8E24AA,stroke:#6A1B9A,stroke-width:2px,color:#fff classDef gl fill:#D32F2F,stroke:#B71C1C,stroke-width:2px,color:#fff

The idea is that all the drawing logic (how a glyph string is composed, where the wavy underline goes, how an image is clipped to the window, how scrolling works) lives in a plain-C file, without a single platform-specific line. And that each platform only has to implement a small contract: about 25 primitive operations of the kind "upload this texture", "draw this quad", "present the frame". That contract is gfxdrv.h. The first driver would be Metal, in mtlterm.m.

The golden rule, the one I imposed on myself and never broke once: xdisp.c is not touched. The redisplay engine computes the glyph matrices exactly as always; I only hook into the drawing interface that already exists. If the experiment went wrong, Emacs was still Emacs.

In hindsight, this was the best decision of the whole project!

Phase 2: the Metal backend and the tyranny of the pixel

With the architecture clear, I dove into Metal. The technical plan was that of any modern text renderer:

  1. Rasterize each glyph just once, via CoreText, into a grayscale texture (a glyph atlas in R8 format).
  2. Draw the text as textured quads that sample that atlas.
  3. Upload images (PNG, JPEG, SVG, GIF) as textures.
  4. Composite the whole frame on the GPU, in a persistent texture, and present it.

On paper, two afternoons. In practice, weeks. The reason has a name: pixel parity.

My success criterion was not "it looks good". It was that the result be identical, pixel for pixel, to the original Cocoa backend. Same binary, with the GPU on and off, and the diff between the two captures had to be practically zero. I built a harness that launched the same Emacs twice, loaded an identical scenario, captured the screen on both and compared them with Python and PIL. The bar landed around 0.055% of differing pixels in the baseline, and anything that strayed from there was a bug to hunt down.

That harness was relentless, and it surfaced a collection of details I had to look at under a magnifying glass:

  • The ink weight. CoreText and my shader applied antialiasing differently.
  • The relief colors (the 3D borders of buttons and the mode-line) were not coming out right.
  • There was an off-by-one in the vertical position of the glyphs.

We should not overlook that the way of drawing is completely different, both in approach and in architecture. That makes the bugs subtle and hard to detect.

Phase 3: the cursor that froze

Of all the bugs, the one that taught me the most was the cursor one.

I wanted animated cursor effects: a ring that expands when it jumps, a comet-like trail, that kind of visual candy the GPU does almost for free. I implemented them as a compositor layer on top of the frame, without touching the buffer content underneath. They worked perfectly... while I was typing. The moment I stopped touching the keyboard, the animation froze halfway. The culprit was Apple's synchronization mechanism, CADisplayLink: it dies at rest, Emacs's event loop does not feed it when there is no user input. While I typed, the keyboard events pumped the run loop and everything ran fine; the moment I stopped, there was no one to move the clock.

The solution was to stop depending on the system and move everything continuous to a Lisp timer. Cursor, buffer cross-fade and video, everything advances from a single "pump" in Emacs Lisp that ticks periodically and tells the driver "advance everything you have and present at most once". Later I unified the three timers into one with auto-pacing (60 Hz when there is a fade, 30 Hz otherwise, and it shuts itself off when there is nothing to animate).

Once this was solved, macOS was complete. Text, decorations, images, animated GIF, line numbers, fringes with custom bitmaps, mode-line, header-line, tab-bar, Retina/HiDPI at 2x, the four cursor types, splits, dynamic text-scale. Everything pixel-perfect against Cocoa.

Now it was time to add the things only the GPU can do:

  • Video inside the buffer
  • Shader-based cursor effects
  • Cross-fade when switching buffers.

As an experiment I even put together a small YouTube frontend inside Emacs: it searched for the video and played it directly in a buffer, with the GPU compositing the frames over the text. A fun little silliness that is only possible when the frame is painted by the graphics card.

And the cross-fade when switching buffers, a smooth fade that on the GPU is just one more shader pass:

It was relatively simple, since the redisplay engine neither knows nor cares what I do on top of it; they are just compositing operations on the GPU.

Phase 4: packaging is half the work

Having the binary working on my machine and having something another person can install are two different planets. This phase has no glamour but it ate whole days.

Apple's signing and notarization were a labyrinth of their own. And when I added native-comp (AOT native compilation), about 1564 .eln files appeared that are also Mach-O code and also have to be signed one by one, with a secure timestamp, for notarization to accept them.

I published the first signed and notarized release, a Homebrew cask, and started using it daily along with a colleague. It worked. I was happy. I thought the hard part was behind me.

Then I decided to show it to the Emacs mailing list.

Phase 5: emacs-devel, or how to learn humility in an email thread

On June 8, 2026 I sent an [RFC PATCH] to emacs-devel with the subject "GPU display backend with a neutral driver layer (Metal on macOS)". I framed it carefully: I was not selling "Emacs with Metal", I was selling the abstraction. A neutral drawing layer plus a thin per-platform driver behind a small vtable, with Metal as the first driver, xdisp.c untouched, parity verified with an automatic harness, and the FSF copyright assignment already on file.

My first mistake was sending the complete patch, rather than an RFC with the idea, the design and a minimal demo. The response came fast:

Sean Whitton wrote:

"People don't normally post such large patches at once without first discussing the design issues with people on the list. Given this, I just have to ask, this isn't LLM-generated, is it?"

I answered honestly:

"100% created with LLM.

I understand that this is a rather large addition, and if it's rejected, I won't be offended. My intention was to share it because it's fully developed [...]; I'm using it daily without any problems (along with other colleagues)."

Whitton's reply was courteous and final:

"I'm afraid there is a policy conflict. The GNU project does not accept any LLM-generated contributions at present. Thank you for your interest in Emacs, anyway."

And there, in terms of "does this get merged?", the project died in less than a day. The GNU project does not, as of today, accept LLM-generated contributions. Period. No technical debate matters when it collides with a hard policy.

What I did not expect is that, far from closing the thread, it opened in three directions at once.

The turn toward "study subject"

Dmitry Gutov set the tone for what was coming:

"We cannot accept this as code contribution, but if you are already using it locally, it might be useful as a study subject. It might be more useful to test with a Linux port, though."

In other words: as code it does not get in, but as a reference or an experiment it might be worth something. And there, too, was the seed of what I would do next.

The freedom debate

And then Richard Stallman stepped in, forking the thread's subject to "GPU-specific code with no GPU-specific features?" and raising it to a moral question:

"In general the GPU is a disaster for software freedom: it turns your computer into a prison."

Later, insisting:

"They don't put physical chains on the user, but they do put digital chains on the user's computing. GPUs are a substantial part of what we are fighting to free people from."

Not everyone bought it. Arsen Arsenović answered with the sharpest technical objection in the thread:

"This is a bizarre comparison based on frivolous word association. GPU programming APIs such as Vulkan or OpenGL [...] can be implemented fully in software, and indeed are implemented using fully free software in Mesa, so there's no downside to using them from this perspective."

And Madhu brought up the uncomfortable fact that dismantles half the discussion:

"If you are using X11 on a modern (say post 2021) intel machine on linux, all your 2d graphics probably goes through the GPU backend, X11 windows are just textures."

They were right about something I could demonstrate: my OpenGL driver also runs on Mesa's software rasterizer (llvmpipe). In fact, the parity suite runs headless on it. In other words, the code does not require non-free GPU firmware to be exercised. I said so in the thread, although by then the debate had a life of its own.

The underlying technical doubt

The most substantive criticism, and the one that made me think the most, was neither the political nor the ideological one. It came from Eli Zaretskii, one of the historical maintainers:

"I'm not really surprised that using a GPU in the display backend yields performance gains that are not really spectacular with reasonable sizes of the frame: the design of the current display engine is optimized towards CPU-driven redisplay, so taking a better advantage of GPUs will probably need a more thorough redesign, not just a separate backend."

And Gerd Möllmann, the redisplay maintainer, finished it off with elegant indifference:

"It looks to me as if this adds GPU support without adding GPU-only features or changing the architecture of redisplay [...]. Can have performance benefits, maybe, don't know, but it's outside of my field of interest."

They were partly right. The engine is built to repaint small dirty rectangles on the CPU, and cairo does that extraordinarily well. Slipping a GPU underneath without redesigning the engine has a ceiling. But they were also partly making a point that can only be seen with a second backend and with numbers. And I had neither one yet.

The community had, without knowing it, written my roadmap for the following weeks: build a second backend (to validate the abstraction) and bring honest numbers (to settle Eli's doubt).

Phase 6: the OpenGL driver, or how to cash in the architecture bet

If my design's big promise was "the neutral layer is reused as-is, you only change the driver", the only way to prove it was to write a second driver from scratch and see how much shared code survived untouched.

I chose GNU/Linux over OpenGL ES 3 with EGL, on X11. The cross-platform counterpart of the Metal driver: I rasterize the glyphs with FreeType into a GPU atlas, render to an FBO and present by blitting to the window surface with eglSwapBuffers. The drawing policy, gfxterm.c, I reused entirely. And it worked: the second backend came out pixel-perfect against the stock GTK/cairo Emacs, on the same exhaustive test battery as macOS, running both on a real X server with a GPU and headless under Xvfb for the harness.

That was the moment the architecture stopped being a promise and became a fact. Writing the whole driver, with all its EGL and FreeType quirks, took me far less than the first one, because all the hard logic was already written and tested in the neutral layer.

But Linux brought its own hell, with the hardest bugs of the entire project. The worst one: when switching buffers, during a single flicker (one vblank), the half-painted startup dashboard leaked through, ghost images of other content. It took me days to discover that the root cause was not my GPU code, but the back buffer of X11's double-buffer extension (XDBE), which Emacs painted at startup and my backend never touched again.

Still, after a while of work and debugging, the OpenGL driver became stable and functional. Not perfect, but good enough to run the performance harness and compare it against cairo.

Phase 7: optimizing and bringing honest numbers

The results, on a laptop with an integrated AMD Radeon (Renoir) GPU, a 1616x912 frame, an 8000-line font-locked buffer:

Workload Stock (X/cairo) GPU (OpenGL) Ratio
Line scroll 530 fps 487 fps 0.92x
Page scroll 297 fps 296 fps 1.00x
Full-frame redraw 247 fps 294 fps 1.19x
Typing 1857 fps 1311 fps 0.71x
Image scroll 1359 fps 1239 fps 0.91x

On a laptop-sized frame, typing and line scroll are still slower than cairo, which is excellent at clipping small rectangles. My floor is one EGL buffer swap per redisplay; cairo's is a tiny damage rectangle with no swapchain. In absolute terms, everything is far above what is perceptible (the worst case, typing, is ~0.8 ms per keystroke), so it is a matter of throughput, not of responsiveness.

In short: a GPU backend does not beat a mature CPU rasterizer at static text.

But the same workloads at 4K (3760x2210) flip the result:

Workload cairo (CPU) GPU Speedup
Line scroll 117 fps 240 fps 2.05x
Page scroll 102 fps 124 fps 1.22x
Full-frame redraw 66 fps 121 fps 1.84x
Typing 238 fps 1766 fps 7.4x
Image scroll 115 fps 1328 fps 11.5x

cairo's cost grows linearly with the pixel count; the GPU's barely moves. Image scroll is the extreme case: cairo re-blits the image from CPU memory every frame, the GPU re-composites an already-cached texture. There, plus the features that only exist on the GPU (video, cross-fades, cursor effects), is where the real value lies. Text throughput on a small screen is not.

Conclusion

Let's be honest, for everyday text on a normal screen there is no reason to switch; the CPU backend is just as fast or faster. The GPU earns its place in motion, effects and video at high pixel counts, things the CPU does more expensively or cannot do at all.

The backend will never be merged into Emacs, and that is fine. It collides with the policy of not accepting LLM-generated contributions, and that is a legitimate decision by the project that I respect. I keep it as a personal fork, behind --with-gpu, opt-in and disablable with an environment variable, and I use it daily.

Was it worth it, then? For me, without a doubt. I came out with a complete display backend on two platforms, with video inside the buffer and effects that stock Emacs cannot draw, with an architecture that proved it could hold up across two implementations, and with a pile of technical scars worth their weight in gold. But above all I came out with something I was not looking for: a public, hard and honest conversation, with people who have maintained this editor for decades, about performance, about software freedom and about the role of AI in the code we write. That conversation, quoted above in their exact words, is worth more than any merge.

Right now I am focusing my efforts on the OpenGL backend for GNU/Linux, where there is still much to polish and optimize. If you are interested in trying it, in the repository you will find a .deb with the test binaries.

My final advice, if you are considering something similar: chase the experiment that obsesses you even if you know it might not end where you imagined. I started out wanting my GPU to draw text and ended up learning about architecture, about rasterization, about packaging, about the culture of a 40-year-old project and about myself. The destination turned out to be irrelevant. The journey did not.

The code is at github.com/tanrax/emacs-gpu, and the full emacs-devel thread can be read in the June 2026 archive.

-1:-- How I built a GPU backend for Emacs (Post Andros Fenollosa)--L0--C0--2026-06-23T09:33:09.000Z

Sacha Chua: 2026-06-22 Emacs news

There was lots of discussion around Rahul's post on Emacs 31. It's the first link in the list below, so I won't repeat the links here. Also, I like visualizations, so I thought these force-directed graphs (Reddit) and text-based mindmaps (Reddit, lobste.rs) were pretty cool. 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!

View Org source for this post

You can e-mail me at sacha@sachachua.com.

-1:-- 2026-06-22 Emacs news (Post Sacha Chua)--L0--C0--2026-06-23T02:30:26.000Z

Alvaro Ramirez: ytr: YouTube radio for Emacs

I've been a happy ready-player user for some time now. I consider the Emacs package fairly feature-complete, for my needs anyway. Well almost.

While I've successfully migrated most of my music-listening to offline playback, there are the odd times when I enjoy streaming YouTube audio. I've pondered extending ready-player for this use case, but its current approach is fairly file-driven. For starters, it uses dired as a core abstraction.

Before venturing on a major refactoring, without even knowing if an Emacs streaming flow would stick, I decided to build a new package. Coincidentally, this enables me to experiment with the package UX without being restricted by ready-player's needs. And so that's what I did in my new YouTube radio package ytr.

ytr really is fairly experimental. I'm currently driving its development purely on current needs. Let's see where it goes. It borrows lots from ready-player, but its UX presents itself more as a widget. I'm kinda liking the experience. There isn't much to it: you add a channel URL, and its content metadata is automatically pulled and presented as a child frame. I've also sprinkled in some eye candy (animations), reminiscing about the Winamp days. Beware, these sweets require running on Emacs GUI.

ytr is powered by mpv and yt-dlp, the real streaming workhorses doing the heavy lifting.

ytr is available on GitHub if you're keen to check it out. Keep in mind this is a brand new package and a first iteration, so it may need some improvements. If you give it a try, I'd love to hear how you got on. I've only tested on macOS so far.

Make it sustainable, sponsor the effort

Liking ytr? Would like to see it evolve? Consider sponsoring the effort.

-1:-- ytr: YouTube radio for Emacs (Post Alvaro Ramirez)--L0--C0--2026-06-23T00:00:00.000Z

Raymond Zeitler: Emacs Carnival: diary, Part 2

As I wrote earlier, I consider diary to be an underappreciated Emacs built-in.

I got interested in it again when I wanted to schedule a recurring meeting in Org for the third Tuesday of every month. I saw on Reddit that some folks use diary-float in place of the usual active timestamp to achieve this. So I added this after the meeting header to make it work:

  * Third Tuesday of the Month Club
  SCHEDULED: <(%%diary-float t 2 3) 19:00-20:00>

I have five meetings that use this method of automatic scheduling. I like this approach because of its simplicity and elegance. Unfortunately, it doesn't behave the same way as a recurring task.

When I mark a recurring task as complete, Org reschedules it for the next date on which it should occur. And it changes DONE to TODO to ensure the event shows up on the agenda for the next occurrence.

This doesn't happen for a task that's scheduled with diary-float. When marked complete, the headline remains at DONE; the next event won't show up on the agenda.

And so Fengyuan Chen wrote next-day-spec1 to solve this issue. Unfortunately it doesn't work with the latest versions of Emacs.

One of the many recognized cognitive biases is called the Sunk Cost Fallacy2, which impels an individual to favor an inferior (or incorrect) solution because a significant amount time, money, and/or effort has been invested in the solution. Well, I've invested time and effort in both diary-float and next-day-spec, so I've continued to endorse it as a solution.

But even if rescheduling did work, I think it's better to have each meeting scheduled as a subheading of the overall meeting topic, like this:

  * Third Tuesday of the Month Club
  ** DONE May 2026: <2026-05-19 Tue>
    :LOGBOOK:
    CLOCK: [2026-05-19 Tue 18:54]--[2026-05-19 Tue 20:12] =>  1:18
    :END:
    - Note taken on [2026-05-19 Tue 21:20] \\
    We ate pizza.  Again.
    ** TODO June 2026: <2026-06-23 Tue>
      ** TODO July 2026: <2026-07-21 Tue>

The advantage to this is that clocked time and notes are organized neatly within each individual subtopic, not aggregated into one logbook and a series of notes. If the aggregated time is desired, one could insert the log report. I'll review this in another post.

But it's fun to schedule events that occur on irregular dates, such as National Engineers Week (US), which is the week in which George Washington's birth anniversary occurs3. Here's how you can add it to an Org file:

  * TODO Celebrate Engineers Week!
  SCHEDULED: <%%(equal (calendar-gregorian-from-absolute (calendar-dayname-on-or-before 0 (calendar-absolute-from-gregorian (list 2 22 (calendar-extract-year date))))) date)>

Do you have a favorite use for dairy?

1 https://github.com/chenfengyuan/elisp/blob/master/next-spec-day.el
2 Sunk cost - Wikipedia
3 https://www.holidayscalendar.com/event/national-engineers-week/
-1:-- Emacs Carnival: diary, Part 2 (Post Raymond Zeitler)--L0--C0--2026-06-22T21:31:52.553Z

Marcin Borkowski: Disabling minor modes with local variables

I’ve known about file local variables for a long time. I also knew about the eval keyword, which can be used as a file variable, but instead of binding the given value to the (non-existent) variable eval, Emacs evaluates it. This is often useful, but the usefulness is diminished by the fact that Emacs nags the user whether it is safe to evaluate a form provided that way. (Of course, it is reasonable security-wise, and that’s why I didn’t set enable-local-variables to :all – I still prefer the nagging to the risk of executing untrusted code!) There is, however, one case where the eval form seems safe and useful enough to grant an exception: enabling or disabling minor modes.
-1:-- Disabling minor modes with local variables (Post Marcin Borkowski)--L0--C0--2026-06-22T17:19:29.000Z

Irreal: Structured Emacs Editing With Builtin Commands

Bozhidar Batsov has an excellent post that considers structured editing using builtin Emacs commands. It turns out that all those commands (like forward-sexp) that you think of as Lisp-specific commands work in many other modes. The secret is that the target mode has to tell the commands how to find structure boundaries.

There are several ways of doing that. The oldest is to use the mode’s syntax table but that doesn’t work if the structure is bounded by things like structureend. The more modern method is to provide functions that can find these boundaries and, of course, the most modern is to use Tree-sitter with an actual grammar of the target language.

Pay particular attention to the Ctrl+Meta commands. If you’ve done any Lisp programming, they’ll be familiar but they’re useful for a lot of other languages—even plane text. For example, I use Ctrl+Meta+Space all the time to progressively mark words in a text buffer. It’s a bit easier than using Ctrl+Space and then some motion command to do the same thing.

As Batsov says, the Ctrl+Meta commands keep getting smarter as more major modes teach them about their syntax. His post is full of good information that I haven’t mentioned here so be sure to head over and read it.

-1:-- Structured Emacs Editing With Builtin Commands (Post Irreal)--L0--C0--2026-06-22T14:15:22.000Z

Monadic Sheep: Hello there!

by Tushar and Divya
2026 June 22

Hello there! This is MonadicSheep’s first blog post. Even though this is our first public post, we have been working on several projects over the last few months. This will be a brief overview of the projects and their developments.

1. Emacs Reader: An all-in-one document reader for GNU Emacs.

This project began in the summer of 2025, around late February/early March. It was the result of Divya’s frustrations with pdf-tools (who had been contemplating this project for a while by then). Some of the frustrations were: insane memory usage (in gigabytes!) which made it hard to read multiple large PDF files, having to go back to DocView for EPUBs and the like. DocView itself suffers from many of the similar issues, and also is less featureful (though it does have some interesting unique features).

It was decided in the initial prototype to rely exclusively on MuPDF, the most efficient and performant PDF engine out there, ahead of Poppler (used by pdf-tools). One thing led to another, and Tushar joined the work as well and bullied pushed Divya into getting Emacs Reader polished and published. Tushar helped immensely in the Emacs Lisp side of things, while Divya took care of the dynamic module and integration with MuPDF.

To learn more about what Emacs Reader can do and see it in action, checkout our Emacs Conf Talk from December 2025.

2. PALE & Canvas Patch

We quickly hit a brick wall in developing Emacs-Reader. Our hacks techniques to get Emacs to display the pages were not cutting it for displaying text highlights on the page. And to at least try to keep emacs-reader a little simple, we decided to extract them into a separate library, PALE: Picture & Animation Library for Emacs, and somehow get text highlighting working… Which we were able to get working by dividing the images into smaller images (tiles).

Soon, Daniel Mendler (minad) looked at our work and suggested a “canvas” patch upstream, which would relieve us all of the hacks in PALE, and Emacs-Reader.

Check this web page for demos.

We are currently finalizing a new version of the Canvas patch with Daniel Mendler to be re-submitted to Emacs Devel. The hope is that it gets merged eventually. Meanwhile, we’re also working on integrating Canvas to Emacs Reader to test a few things.

3. Insidious: Youtube Client

This project is an Emacs client for YouTube for fetching video search results. Tushar started working on it because he wanted to have search results from multiple pages, which Yeetube, another package for doing the same, didn't support at the time.

It was submitted for NonGNU ELPA but it wasn't merged because of software freedom concerns: the package, when playing videos, invokes mpv which invokes yt-dlp, which could execute non-free Javascript on the user's system.

The development is currently slow. But we have plans to revive the project in the future, possibly integrating with PALE/Canvas Patch to display the videos directly inside Emacs buffers.

-1:-- Hello there! (Post Monadic Sheep)--L0--C0--2026-06-22T00:00:00.000Z

Irreal: Let Emacs Teach You Emacs

Charlie Holland has a followup to his post on Emacs Help that I wrote about the other day. It’s a reconsidered, condensed version of the original post. As Holland puts it,

This is a condensed version of my very long June Emacs Carnival post on the same topic. I wanted to make this as I felt it would be more intuitive for beginners, who are most likely to benefit from learning Emacs’s auto-discovery features.

It actually is more useful both for n00bs and experienced users alike. The best example of that is his explicit calling out of what he calls “the prefix trick”. The idea is that there are a handful of prefixes that begin many help commands. One example is describe- that begins all the help commands that describe something: describe-function, describe-variable, etc. The “trick” is that you can type Meta+x describe and then Tab to get a list of all the describe commands. That’s something that most of us know implicitly but it’s useful to have it pointed out explicitly.

The other useful fact, which I wasn’t aware of, is that Meta+x which-key-show-top-level will show all the commands without a prefix.

The point of Holland’s followup is that you don’t need to memorize a lot of arcana to use the Emacs Help system. If you know the prefixes to use with Meta+x and Ctrl+h Ctrl+h to bring up the Help cheat sheet, you’re good to go.

Update [2026-06-23 Tue 11:15]: Added link to followup.

-1:-- Let Emacs Teach You Emacs (Post Irreal)--L0--C0--2026-06-21T15:05:16.000Z

Protesilaos: Emacs: modus-themes version 5.3.0

I just published the latest stable release of the Modus themes. The change log entry is reproduced further below. For any questions, you are welcome to contact me. I will now work to apply these same changes to emacs.git, so please wait a little longer for the updates to trickle down to you.


5.3.0 on 2026-06-21

The Modus themes are in a stable state. To my mind, they provide the best “default theme” experience across the 40+ original themes I have carefully designed.

Modus is also a platform for making themes for Emacs. There are plenty of derivatives already. I am linking to them through the project’s README.md and am always happy to mention more packages—just let me know.

This version does not include many user-facing changes. Most of my work focused on making internal refinements.

Load the themes through the early-init.el

The Modus themes can now be loaded through the early-init.el file. The idea is to avoid the flash of light that occurs under certain conditions during startup.

This feature is the result of several changes to helper functions, especially those contributing to the modus-themes-generate-palette function that Modus derivatives may be relying on.

Thanks to Steven Allen for an intermediate refinement in pull request 194 and to Mike Olson for another relevant tweak in pull request 199:

Steven’s contribution no longer exists in the code base due to other changes I made, but was still useful at the time.

Both changes are small, meaning that their authors do not need to assign copyright to the Free Software Foundation.

Also thanks to Jacod “Jake” Gordon for reminding me to apply one of the new functions to the org-habit faces. This was done in issue 197: https://github.com/protesilaos/modus-themes/issues/197.

The underline for widget fields has the correct colour

Relevant faces use the palette entry for border (invoke the command modus-themes-preview-colors or modus-themes-preview-colors-current to view the entries in a theme’s palette).

A popular package that makes use of widget.el widgets is notmuch.

Thanks to ukiran03 for the contribution, which was done in pull request 193: https://github.com/protesilaos/modus-themes/pull/193. The change is small, meaning that ukiran03 does not need to assign copyright to the Free Software Foundation.

A palette can now have a bg-popup entry

This gives users and derivative themes the option to pick a suitable value for popup interfaces, such as those of the company and corfu packages.

Thanks to aikrahguzar for making this suggestion in issue 70 of my ef-themes repository (the ef-themes are built on top of the modus-themes since Modus version 5.0.0, Ef version 2.0.0): https://github.com/protesilaos/ef-themes/issues/70.

Support for faces or packages

  • lin by Protesilaos.
  • pulsar by Protesilaos.
  • institution-calendar by Protesilaos.
  • markdown-ts-mode by Rahul Martim Juliato and Stéphane Marks.
  • trust-manager by Eshel Yaron.
  • typst-ts-mode by Ziqi Yang. Thanks to Pranshu Sharma for suggesting its inclusion in issue 208: https://github.com/protesilaos/modus-themes/issues/208.
  • new elfeed faces since the maintenance of the project was assumed by Daniel Mendler, Karthik Chikmagalur, and Ihor Radchenko. To this end, thanks to Steven Allen for pull request 217 that added the elfeed-show-* faces: https://github.com/protesilaos/modus-themes/pull/217

Internal refinements to the modus-themes-with-colors macro

It now correctly handles the order of default palette colours and user-defined palette overrides. Thanks to JD Smith for the contribution in pull request 191: https://github.com/protesilaos/modus-themes/pull/191.

The change is small, meaning that JD does not need to assign copyright to the Free Software Foundation.

The multiple-cursors are fine even when a bar is used

When the cursor-type is configured to be a bar, the fake cursors produced by the multiple-cursors package will still look right. Thanks to Elias Gabriel Perez for the change to the mc/cursor-bar-face in pull request 213: https://github.com/protesilaos/modus-themes/pull/213.

The blink-matching-paren-offscreen is the same as show-paren-match

This is for thematic consistency. Thanks to Troy Brown for suggesting this change in issue 209: https://github.com/protesilaos/modus-themes/issues/209.

Get vc-annotate look right

The built-in vc-annotate command relies on a user option to read color values. We cannot handle this nicely at the theme level. Users need to write their own configuration like this:

(defun my-modus-vc-annotate (&rest _)
  (modus-themes-with-colors
    (setq vc-annotate-background-mode nil)
    (setq vc-annotate-very-old-color fg-dim)
    (setq vc-annotate-color-map
          `(( 20. . ,red)
            ( 40. . ,red-cooler)
            ( 60. . ,red-warmer)
            ( 80. . ,yellow-warmer)
            (100. . ,yellow)
            (120. . ,yellow-cooler)
            (140. . ,green-warmer)
            (160. . ,green)
            (180. . ,green-cooler)
            (200. . ,cyan-cooler)
            (220. . ,cyan-warmer)
            (240. . ,cyan)
            (260. . ,blue-warmer)
            (280. . ,blue)
            (300. . ,blue-cooler)
            (320. . ,blue-intense)
            (340. . ,magenta-cooler)
            (360. . ,fg-dim)))))

(with-eval-after-load 'vc-annotate
  (my-modus-vc-annotate)
  (add-hook 'enable-theme-functions #'my-modus-vc-annotate))

[ The above code is relevant as of this writing. Though remember that I do not keep older publications up-to-date. The only source of truth is the manual of the Modus themes. ]

Two old user options are no longer needed

The user options modus-themes-completions and modus-themes-prompts are obsolete. They used to be relevant before the introduction of palette overrides.

Rewrote large parts of the manual

I did it for clarity, but also to remove notes that were specific to older versions of Emacs.

Many new ERT tests for the project

I have written many tests. They do not cover every single function, though the plan is to do that eventually. These tests are important to ensure that Modus is a solid platform for making derivative themes.

Much of this was done live: https://protesilaos.com/codelog/2026-04-10-emacs-spontaneous-live-modus-themes/.

Thanks to Benjamin Kästner for a couple of tweaks to a relevant macro in the tests’ file. This was done in pull request 212, with further changes by me: https://github.com/protesilaos/modus-themes/pull/212.

Links to projects related to Modus

In the README.md I now mention projects that are related to the Modus themes, such as derivative Emacs themes, but also ports for other editors.

There is also a link to my modus-themes-exporter package, which I developed during a live stream:

Git commits

git shortlog 5.2.0..5.3.0 --summary --numbered
    123	Protesilaos
     2	Benjamin Kästner
     2	Steven Allen
     1	Elias Gabriel Perez
     1	JD Smith
     1	Mike Olson
     1	ukiran03
-1:-- Emacs: modus-themes version 5.3.0 (Post Protesilaos)--L0--C0--2026-06-21T00:00:00.000Z

Protesilaos: Emacs: ef-themes version 2.2.0

The ef-themes are a collection of light and dark themes for GNU Emacs that provide colourful (“pretty”) yet legible options for users who want something with a bit more flair than the modus-themes (also designed by me).

Below are the release notes.


Version 2.2.0 on 2026-06-21

This version contains two new themes and several stylistic refinements to existing items in the collection.

Enjoy ef-arcadia and ef-atlantis themes

ef-arcadia is a light theme with a green, humid feel. ef-atlantis is a dark theme with aquatic colours.

Improved style for company and corfu popups

I have revised the colour that each theme applies to the popup background. It should now be more consistent with all other elements on display.

Thanks to aikrahguzar for suggesting a review in issue 70: https://github.com/protesilaos/ef-themes/issues/70.

The current line highlight for completions is easier to spot

This is the background of the selected candidate in the minibuffer while using the vertico package (among other similar interfaces). The colours I am now using are more consistent with their context and also work better as part of a popup completion interface, as noted above.

Palette refinements for several themes

I have made subtle changes to a few colour values. These are fine details. The overarching goal is to be consistent throughout.

-1:-- Emacs: ef-themes version 2.2.0 (Post Protesilaos)--L0--C0--2026-06-21T00:00:00.000Z

Irreal: Changes Coming In Emacs 31

Rahul M. Juliato has an informative post on what’s coming in Emacs 31. Juliato doesn’t mind living on the edge and has been tracking and using Emacs from the Master and emacs-31 branches. He says he likes to know “what’s in the box” and the way to learn what’s coming is to is to live in it. That’s a little too exciting for me and perhaps for you but Juliato’s post gives us a view of what to expect.

The list is a long one and his post reflects that. It’s a lengthy post but it does a good job of describing what to expect. The thing he’s happiest about is that with this release tree-sitter “just works”. Now installing new grammars is pretty much automatic and there are grammars for more languages.

For those of you who have to—or for some reason, choose to—use Markdown instead of Org mode, the new markdown-ts-mode will be a welcome addition. The new mode has an Org mode feel so Org users will feel comfortable using it.

For those of you who like the completion system making your mind up for you, there are many enhancements to eager completion. That definitely not something I would want; I hate having the system change what I’m typing without my asking for it.

There are several other changes. Check out Juliato’s post for the details. It looks to be a nice release with plenty of improvements. People like Juliato help the development process by thoroughly testing new features for bugs and for suggested improvements.

The minions are insisting that I let you know that you can and should view Juliato’s post in light mode by flipping the switch at the top of the page. Sorry for the boring announcement but none of us wants to deal with disgruntled minions.

-1:-- Changes Coming In Emacs 31 (Post Irreal)--L0--C0--2026-06-20T14:24:26.000Z

Emacs Redux: Essential Structured Navigation and Editing Commands

Most of us learn Emacs one motion at a time – C-f for a character, M-f for a word, C-n for a line. Useful, but those commands don’t know anything about the structure of your code. Emacs has a whole other family of commands that operate on balanced expressions and definitions instead, and once they become muscle memory they’re hard to give up.

Lisp hackers know these commands intimately – they’re the foundation paredit builds on. What’s less appreciated is that they work in plenty of other languages too. I’ve been leaning on them heavily while building neocaml, my tree-sitter package for OCaml programming, so I’ll use OCaml for the examples here. The commands themselves are general, though, and most of what follows applies to any structure-aware major mode.

Sexps and defuns

Two terms show up everywhere in this corner of Emacs, and both are inherited from its Lisp roots:

  • A sexp (“symbolic expression”) is a balanced expression: a literal, an identifier, a parenthesized group, a list, a function application – whatever the major mode considers one self-contained unit.
  • A defun is a top-level definition. In Lisp that’s a defun; in OCaml it’s a let binding, a type definition, a module, and so on.

The names are Lispy, but the concepts are general, and that’s the whole point. Every command below is built on one of these two notions.

Moving around

These are the everyday workhorses. In the examples, marks point (the cursor).

Keybinding Command What it does
C-M-f forward-sexp Move forward over a balanced expression
C-M-b backward-sexp Move backward over a balanced expression
C-M-d down-list Move into a bracketed group
C-M-u backward-up-list Move out of the enclosing group
C-M-a beginning-of-defun Jump to the start of the current definition
C-M-e end-of-defun Jump to the end of the current definition

The interesting thing about forward-sexp is that it moves over the largest expression starting at point. At the head of a function application, that’s the whole call:

let r = List.map (fun x -> x + 1) numbers

After C-M-f:

let r = List.map (fun x -> x + 1) numbers

But inside a collection it steps over one element at a time, which is exactly what you want when you’re editing the elements:

let xs = [ aa; bb; cc ]
let xs = [ aa; bb; cc ]

down-list and backward-up-list are a great pair for repositioning. The latter is especially nice in a structure-aware mode because it understands keyword-delimited blocks, not just parens. From deep inside a struct body, C-M-u climbs out to the enclosing module:

module M = struct
  let x = 1
  let y = 2
end
module M = struct
  let x = 1
  let y = 2
end

Editing with structure

Movement is only half the story. The same structural knowledge powers a set of editing commands that are far more precise than their line- or character-based cousins.

Keybinding Command What it does
C-M-SPC mark-sexp Mark the next balanced expression
C-M-k kill-sexp Kill the expression after point
C-M-t transpose-sexps Swap the two expressions around point
M-x delete-pair delete-pair Remove a matched delimiter pair
M-x raise-sexp raise-sexp Replace the enclosing expression with the one at point

kill-sexp removes a whole expression without you having to hunt for where it ends:

let xs = (List.rev ys) @@ tl
let xs =  @@ tl

transpose-sexps swaps the expressions on either side of point – a one-keystroke way to fix arguments you passed in the wrong order:

let path = Filename.concat dir file
let path = Filename.concat file dir

(There’s much more to say about transposition – I covered the whole family in Transpose All The Things.)

delete-pair unwraps a bracketed group: put point on the opening delimiter and it deletes both it and its match.

let area = pi *. (r *. r)
let area = pi *. r *. r

delete-pair has a subtle dependency on structure that I ran into head-on while working on neocaml – it relies on the mode’s notion of a sexp to find the matching delimiter. I wrote that adventure up separately in Removing Paired Delimiters in Emacs.

Finally, raise-sexp replaces the enclosing expression with the sub-expression at point. It’s the fastest way I know to strip a wrapper – here, dropping a Some around a call:

let result = process (Some (find_opt key tbl))
let result = process (find_opt key tbl)

One caveat: raise-sexp needs something to raise into. Invoke it at the top level of a binding, where nothing surrounds the expression, and it’ll complain about unbalanced parentheses. Reach for it inside a call, a list, or a parenthesized expression.

The same key, different behavior

Here’s the part that trips people up: these commands behave differently from one major mode to the next, because each mode gets to define what “an expression” or “a definition” actually means. They do it through a few buffer-local hooks – chiefly forward-sexp-function (which backs C-M-f/C-M-b and everything built on them, including kill-sexp, transpose-sexps, and delete-pair) and beginning-of-defun-function / end-of-defun-function for defun motion. What plugs into those hooks has changed a lot over the years.

The classic backing: syntax tables and regexps

For most of Emacs’s history, structural commands rested on the syntax table – the per-mode table that classifies each character as an open or close delimiter, a string quote, a word or symbol constituent, and so on. scan-sexps and scan-lists walk the buffer counting delimiters according to that table, and that’s what forward-sexp falls back on when a mode sets nothing special. It’s wonderfully cheap and works in any buffer, even plain text – but it’s purely lexical. It counts brackets and respects quoting, yet has no idea what a function or a block actually is. That’s why vanilla backward-up-list can’t climb out of a begin ... end or OCaml struct ... end block: those aren’t bracket characters, so as far as the syntax table is concerned they don’t exist.

Modes that wanted more had two classic options:

  • SMIE (the Simple Minded Indentation Engine). Modes like ruby-mode and octave-mode feed SMIE a small operator-precedence grammar, and smie-setup points forward-sexp-function at smie-forward-sexp-command. Suddenly C-M-f understands keyword-delimited blocks like if ... end, not just parens.
  • Regexps for definitions. A beginning-of-defun-function that scans for a header pattern, and an imenu-generic-expression for the imenu index. Quick to write, but heuristic – easily fooled by code inside strings and comments, or by unconventional formatting.

The tree-sitter backing: a real parse tree

Tree-sitter modes answer the same questions from an actual concrete syntax tree. forward-sexp-function becomes treesit-forward-sexp; on Emacs 30+, a mode declares named “things” – sexp, list, sentence, defun, text – via treesit-thing-settings, and the navigation commands consult them. Defun motion goes through treesit-beginning-of-defun (driven by treesit-defun-type-regexp), and imenu through treesit-simple-imenu-settings.

Because it’s the real grammar rather than a delimiter count, it’s accurate exactly where the lexical approach has to guess. That’s why, earlier, C-M-f could treat the whole List.map ... application as one node, and C-M-u could climb out of a paren-less struct block – and why a stray brace inside a string never throws it off. The price of admission is a compiled grammar and a reasonably recent Emacs.1

This layering is also why a command like delete-pair can quietly misbehave: it leans on the mode’s forward-sexp-function to find the matching delimiter, so if that disagrees with the buffer’s syntax table you get surprising deletions. Getting these foundations right is a big part of what makes a structure-aware major mode feel solid.

What about paredit?

As mentioned in the beginning of this articles, all of this probably reminds (some of) you of paredit – and it should. paredit takes the same structural ideas and turns them up to eleven, adding slurping, barfing, splicing, and the famous guarantee that your parens always stay balanced. The built-in commands here are the humbler, mode-agnostic ancestors of those ideas.

For non-Lisp languages there are spiritual successors built on tree-sitter. Combobulate is the most ambitious – a full structural editing and navigation package for tree-sitter modes, with drag, splice, and “move by sibling” commands. puni takes a different tack, offering paredit-style soft deletion that works across many languages by leaning on forward-sexp-function. (If you do mix paredit with other tools, mind the keybindings – I wrote about some common clashes in Paredit Keybinding Conflicts.)

More than movement and editing

Once a mode knows how to find sexps and defuns, a surprising number of other features come along for the ride – all powered by the same structural framework:

  • narrow-to-defun (C-x n d) – narrow the buffer to just the current definition. Wonderful for focusing on one function at a time, and it uses the exact same defun detection as C-M-a/C-M-e.
  • which-function-mode – show the name of the definition point is in, in the mode line. It needs the mode to know where definitions begin and end.
  • imenu – jump to a definition by name. Same structural underpinning.
  • Folding – outline-minor-mode, hs-minor-mode, and the tree-sitter outline support on Emacs 30+ all fold along structural boundaries.
  • Expanding the region – repeatedly hitting C-M-SPC grows the selection by sexp, and packages like expreg generalize that to grow by structural node.

Wrapping up

The C-M--prefixed commands are some of the best returns on investment in all of Emacs. They’re not Lisp-only, they’re not tree-sitter-only, and they quietly get smarter as major modes teach Emacs about their syntax. Learn the handful in the tables above – C-M-f/C-M-b, C-M-u/C-M-d, C-M-SPC, C-M-k, C-M-a/C-M-e – and you’ll feel the difference in every language you touch.

And if a couple of these (delete-pair, raise-sexp) feel essential but lack default bindings, give them keys in your major mode of choice. Your future self, mid-refactor, will thank you.

That’s all I have for you today. Keep hacking!

  1. Tree-sitter support was introduced in Emacs 29, but it got significantly better in Emacs 30. 

-1:-- Essential Structured Navigation and Editing Commands (Post Emacs Redux)--L0--C0--2026-06-20T07:00:00.000Z

Please note that planet.emacslife.com aggregates blogs, and blog authors might mention or link to nonfree things. To add a feed to this page, please e-mail the RSS or ATOM feed URL to sacha@sachachua.com . Thank you!