Org Mode requests: [FR] ox-md: Export custom-id search strings (::#) as anchors
-1:-- [FR] ox-md: Export custom-id search strings (::#) as anchors (Post Org Mode requests)--L0--C0--2026-05-11T03:15:59.000Z
-1:-- [FR] ox-md: Export custom-id search strings (::#) as anchors (Post Org Mode requests)--L0--C0--2026-05-11T03:15:59.000Z
Marcin Borkowski (mbork) has an interesting post on the describe-personal-keybindings command. The idea is that the command lists the keybindings that you have set in your configuration. It’s convenient, mbork says, for checking that new Emacs releases haven’t stolen one of your bindings. It’s also interesting to see what bindings you’ve added and what, if anything, they’ve replaced.
But there’s a catch. In order for a personal keybinding to show up, it must have been set with the bind-key macro. That’s a problem for those of us who are long term users. Those who use use-package exclusively have no problem since the :bind command uses bind-key automatically but bindings set with, say, define-key will not appear in the describe-personal-keybindings output.
That’s inspired mbork to refactor his init.el to use use-pacjkage and for stand-alone bindings, the bind-key macro.
The minions are insisting that I mention what they consider the best part of mbork’s post. That, of course, concerned dark mode. Mbork begins his post by mentioning a Web app that provides a Web based cheat sheet of Emacs commands. Mbork says it’s a cool command but not for him because
if I were to create something like that, it would run in Emacs and not in the browser, it would definitely mention transpose-.* commands, and it would never be dark-mode-only;-).
The minions haven’t been causing much trouble lately so I thought it only fair to indulge their desire to get mbork’s dislike of dark mode on the record.
-1:-- Personal Keybindings (Post Irreal)--L0--C0--2026-05-10T14:52:57.000Z
I have used EWW in Emacs for some of my browsing for a while and it does the job very well unless browsing single page applications or other sites that do not send HTML unless you run JavaScript. Thank you Lars Magne Ingebrigtsen for making EWW! Hitting R filters out all the noise for a really pleasant reading experience. It is good to browse within Emacs with no context switching in a keyboard-driven way.
(Apropos they joys of context switching: When I am a bit tired, I use Emacs keybindings everywhere. Tap-to-click is naturally off on my laptops to prevent insanity. I am often able to "help" my students a lot within just a few seconds of random keybindings and clicking everywhere on their laptops. It is really good for students to learn patience and tolerance towards people unable to handle computers. They need that for their user support class and future work in the IT industry.)
When Joshua Blaise wrote about his eww setup and use, I read it with interest and stole most of his ideas for my own config. The important one for this little hack is that I set up URLs with endings like .mp3, .mp4, .m4v, .mkv etc to launch in mpv when browsing to them. This makes watching videos and listening to audio content easy with EWW. EWW is also good for browsing local files by hitting W with point on a file in dired.
In dired, I usually launch external programs for photo editing, media playback etc by pressing & with point on a file name. This brings up the completing-read interface in the minibuffer which asks me if I would like to use dired's guess as to which program to use (which I have set to guess xdg-open first) or something else. I press return and then mpv, gimp, darktable or whatever launches with the file.
I had an html file in my downloads folder which I launched with W to read in EWW. I then, by mistake, hit W when on a .mp3 file, and it launched in mpv. Since W launches the file in EWW and EWW was configured to open .mp3 files in mpv, it did that without asking me which program to use. It is faster to hit W than to hit & RET, or in worst case write a program name and RET. So now I launch media files with W in dired in stead of using &. It speeds things up a bit.
(While writing this, I also remembered that I have functions for playing enclosure links and links in elfeed through mpv that I might replace or improve by shuffling the links to EWW or einar-browse-url-mpv. I wrote my configuration for elfeed very early in my Emacs journey, without really understanding any of it, by copying snippets from the Emacs wiki, blogs and Reddit so it is high time to look at it again anyway. I can probably simplify it.)
Here are the relevant parts of my configuration for EWW to get this working:
(defun einar-browse-url-mpv (url &rest _args) "Opens URL in mpv." (start-process "mpv" nil "mpv" url)) (use-package eww :config (setopt browse-url-handlers '(("\\(youtube\\.com\\|youtu\\.be\\|vimeo\\.com\\|twitch\\.tv\\)" . einar-browse-url-mpv) ("\\.mp3$" . einar-browse-url-mpv) ("\\.mp4$" . einar-browse-url-mpv) ("\\.webp$" . einar-browse-url-mpv) ("\\.m4v$" . einar-browse-url-mpv) ("\\.mkv$" . einar-browse-url-mpv) ("\\.pdf$" . einar-browse-url-pdf) ("." . eww-browse-url))))
-1:-- Speed improvement hack for dired with EWW (Post Einar Mostad)--L0--C0--2026-05-10T14:47:00.000Z
Those of us who have been around long enough remember Sacha Chua’s Emacs Chat videos. The last one was a decade ago but now that Chua’s daughter is a bit older, she’s decided to resurrect them. The first new episode, Emacs Chat 21, is with Amin Bandali with whom she’s worked on EmacsConf for the last 7 years.
The format is always the same. Chua and her guest discuss the guest’s Emacs configuration and how they’ve solved various problems. There’s way too much material for a quick recapitulation—the video is an hour and 12 minutes long—but there’s a transcript at the link so you can go through it at your leisure if you find the video’s pace too rapid.
There were a couple of things that I found particularly interesting. The first is the upcoming user-lisp-directory. It allows you to specify a directory for your Lisp files and Emacs will automatically compile them and add them to the load-path for you. Bandali uses it as a replacement for the package system because he prefers to configure things manually.
The second interesting thing for me was his use of EXWM (Emacs X Window Manager), which goes a long way towards the dream of bringing everything into Emacs. I’ve long wanted to try it out but as its name suggests, it works with the X-Windows system only. The Irreal bunker has famously standardized on Macs so EXWM is unavailable to us. I’d still love to try it though.
There’s a lot of material in the chat so you’ll have to watch it—or at least skim the transcript—to get the whole picture. There are links to his configuration so you can steal anything that seems useful to you.
I’m really happy to see Chua resurrecting her Emacs Chats. I really enjoyed the previous ones and learned a lot from them and I’m sure that will be true of the new ones too.
-1:-- Emacs Chat 21 (Post Irreal)--L0--C0--2026-05-09T15:14:22.000Z
I've done a small update to
make-phony.el, my little
Emacs package that lets me be lazy when adding .PHONY to
mark a target as a phony
target
in a Makefile.
The only change is a tweak to the regular expression, to tighten up the check for a target that can be marked.
-1:-- make-phony.el v1.4.0 (Post Dave Pearson)--L0--C0--2026-05-09T07:36:11.000Z
-1:-- describe-personal-keybindings (Post Marcin Borkowski)--L0--C0--2026-05-09T03:53:14.000Z
As returning readers will know, I produce (or try to produce) regular builds of Emacs for Windows. When things work, these produce pre-compiled binaries as an installer and unpack-and-run zip files. You can find links to the latest set for each branch in the box at the top-left each page on the site.
Today's post is about one program out from the middle of the bucket-brigade of data I have created, generally in an effort to avoid unneeded queries of the upstream (Savannah or Savannah mirrors hosted) repositories.
-1:-- last-rev.pl (Post Corwin Brust)--L0--C0--2026-05-08T18:59:59.000Z
Carrying on with the theme of being lazy while editing
posts, I've released
blogmore.el v4.5.0. This version
adds blogmore-set-as-cover. With this, if you place point on a line that
is an image and run the command, it is set as the cover for the post.
Sure, it's not like it's hard to copy, move, insert a new line, type
cover: and then paste the text, but this is faster and more accurate.
And I'm lazy.
And I like hacking on Emacs Lisp that makes my workflow flow faster.
-1:-- blogmore.el v4.5.0 (Post Dave Pearson)--L0--C0--2026-05-08T18:27:54.000Z
The shell-pop Emacs package provides on-demand access to a terminal buffer via a single, configurable key binding. It allows toggling a terminal window without disrupting the workspace layout, making it a useful tool for quick command-line tasks.
NOTE: Kazuo Yagi, the shell-pop original author, appointed me as a co-maintainer of shell-pop Emacs package. I recently refactored shell-pop to improve robustness, fix bugs, and add support for additional terminals (vterm and eat). Stepping into the maintainer role gave me the opportunity to give the codebase a thorough refactoring.

Adding shell-pop to your workflow offers the following benefits:
shell-pop-full-span is set to set to t)To install shell-pop from MELPA:
(use-package shell-pop
;; :bind automatically sets up the keybinding AND tells Emacs to lazy-load the
;; package the moment the key is pressed.
:bind (("C-c t" . shell-pop))
:custom
;; The key sequence used to toggle the shell window.
(shell-pop-universal-key "C-c t")
;; Sets the screen position where the shell popup appears.
;; You can choose "bottom", "top", "right", "left", or "full".
(shell-pop-window-position "bottom")
;; If non-nil, the window stretches across the entire frame width.
(shell-pop-full-span nil)
;; The path to the shell executable used by the terminal emulator
;; (e.g., "/usr/bin/env bash").
(shell-pop-term-shell shell-file-name)
;; The height or width of the window as a percentage of the frame.
(shell-pop-window-size 30)
;; Setting this to non-nil sends commands to the shell. This is not always
;; desirable, as it can send commands to any prompt.
(shell-pop-autocd-to-working-dir nil))
Here are the exact configurations for the most popular Emacs shells. Simply copy and paste your preferred option into your init file:
(with-eval-after-load 'shell-pop
(setopt shell-pop-shell-type '("ansi-term" "*ansi-term*"
(lambda ()
(ansi-term shell-pop-term-shell)))))
(with-eval-after-load 'shell-pop
(setopt shell-pop-shell-type '("terminal" "*terminal*"
(lambda ()
(term shell-pop-term-shell)))))
Note: Requires the vterm package to be installed.
(with-eval-after-load 'shell-pop
(setopt shell-pop-shell-type '("vterm" "*vterm*"
(lambda ()
(when (fboundp 'vterm)
(let ((vterm-shell shell-pop-term-shell))
(vterm)))))))
Note: Requires the eat package to be installed.
(with-eval-after-load 'shell-pop
(setopt shell-pop-shell-type '("eat" "*eat*"
(lambda ()
(when (fboundp 'eat)
(eat shell-pop-term-shell))))))
If you are a long-time shell-pop user, here are the changes I recently made to the package. I encourage you to try the latest version and send Kazuo Yagi and me your feedback in the issue tracker:
Taking on a maintenance role for a tool I use daily has been an interesting experience. These recent updates aim to make shell-pop more reliable and modern. I encourage you to try the latest version and send us your feedback.
-1:-- Easily Toggle an Emacs Terminal with a Single Keystroke using shell-pop (Recently Refactored) (Post James Cherti)--L0--C0--2026-05-08T14:41:33.000Z
As I mentioned a few days ago, the Emacs 31 release process was on the verge of starting. Now Sean Whitton writes to tell us that the Emacs 31 branch has been created and that the feature freeze has begun.
This means that the Emacs 31 release cycle has begun and that’s what on the Emacs 31 branch is pretty much—modulo bug fixes and last minute emergency additions—what the final release will be like.
The release cycle—if you believe in history—is apt to be long but even though that can be frustrating it means that the final product is going to be rock solid.
Although I keep repeating it, I can’t say it enough: thanks to all the developers who selflessly donate their time and efforts to making and keeping Emacs one of the best—if not the best—editors available.
-1:-- The Emacs 31 Branch Has Been Created (Post Irreal)--L0--C0--2026-05-08T14:10:46.000Z
In the recent weeks I've been engaging Prot as an Emacs coach to help
with doing review passes over my upcoming ffs package as I work on
polishing and documenting it in preparation for offering it for
inclusion in GNU ELPA.
Yesterday we had our second session focused on ffs, which I recorded
and share publicly with everyone with Prot's permission, so that
others can also benefit from Prot's insights and experience as we
discuss various aspects of Emacs package development with the concrete
example of ffs.
Here is the video recording of our session:
You can view or download the full-resolution video from the Internet Archive.
I addressed most of Prot's feedback about ffs from our first
session, and I'll be working on the changes we discussed in this
session in the next days.
In the last third of the video we switched topics to discuss a few
Emacs-related tangents including adding a 'padding' effect for the
mode line and its constructs, and distilling and separating the
easily-reusable package-like parts of one's Emacs configuration from
the actual configuration of those parts (e.g. the distinction of
prot-lisp and prot-emacs-modules in Prot's Emacs configuration).
For mode line padding, here is the snippet I'm using with Prot's
doric-themes:
(doric-themes-with-colors
(custom-set-faces
`(mode-line
((t :box (:line-width 6 :color ,bg-shadow-intense))))
`(mode-line-inactive
((t :box (:line-width 6 :color ,bg-shadow-subtle))))
`(mode-line-highlight
((t :box (:color ,bg-shadow-intense))))))
Take care, and so long for now.
-1:-- FFS code review with Protesilaos (Post Amin Bandali)--L0--C0--2026-05-08T02:10:33.000Z
I met with Amin Bandali to talk about Emacs, specifically Amin’s
upcoming ffs package. Amin informed me about changes to ffs in
light of a discussion we had during a previous session.
Amin asked me to record the meeting and then publish it, which I happily agreed to. You can watch it on Amin’s website: https://kelar.org/~bandali/gnu/emacs/ffs-code-review-prot.html.
[ NOTE: I normally do not share anything about my meetings with people. Not who they are nor what we talk about. ]
Thanks to Amin for making this happen! I am looking forward to new developments.
By the way, I learnt about the function x-export-frames from a
mention in Amin’s ffs package, which led me to write
buffer-to-pdf: https://github.com/protesilaos/buffer-to-pdf.
-1:-- Emacs coaching with Amin Bandali (Post Protesilaos)--L0--C0--2026-05-08T00:00:00.000Z
: Transcript, yay!
I chatted with Shae Erisson about Emacs, keyboards, Org Mode, and life.
View it via the Internet Archive, watch/comment on YouTube, read the transcript online, download the video / MP3 / transcript, or e-mail me your thoughts!
Shae: This is an Ergodox Infinity, but there's a lot of other keyboard flavors like this. And one of the things that I particularly like about this... So around the same time I met Jeff Raskin, who wrote the Inhumane Interface. And so for this particular thing, this is like Control and Alt and Hyper and Super and Shift. And this means that under one thumb, I have a lot more modifier keys than you get off of a standard. And it also means... A lot of my problems started with Emacs pinky, the dreaded, the infamous... I think that one of my... I made a keyboard layout called "I am not koala." You may not know this, but koalas have two thumbs. They have one on each side. And that's cool, but I don't have two thumbs, and I realized that when I was trying to grab something, I didn't put my pinky on it. That would be silly, right? I want to put my thumb around it. And so I decided I would move all of my chording keys under my thumbs. And that's kind of how I...
Shae: Another thing I ended up switching to was I started using trackballs. Oh yeah, yeah. I tend to go completely overboard when trying out new things, so I bought 20 different models of trackballs and ended up settling on this one. The nice thing about this one is that this is how you scroll, and it has four buttons. Sacha: That is really cool. I like using ThinkPads, so I've been just living off the tiny little mouse in the middle of the keyboard. But back in the day, I also used a trackball. If I can get to the point where I want to take my hands off the keyboard again in order to do mouse things, that would probably be the direction I would go.
Shae: And so one of the things that my coworker wrote was this nifty thing called ShWiM. And it's basically "shell with me." And it's a wrapper around TTY share so that with one single command, you can share a terminal. And the way that we use this is... We both run Emacs as a server, and then we use emacsclient in the terminal to connect.
Shae: I don't know if you've ever done this, but I can have a terminal right next to this, and if I run emacsclient in a window, then I'm sharing the same thing. This is a graphical chat with Sacha, in the terminal or in the UI, and both of them are updated. Sacha: That's fantastic. I remember people were using tmate for something similar before where you could share that. But yeah, it's just making it seamless, making it frictionless. And on the other side, I have also just been using wormhole to send large files back and forth between Karthik and John Wiegley because we have this other Emacs chat thing where we're going to post it eventually, once I finish figuring out how to redact all the personal information and Org files. But yeah, it's great for being able to send things without having to worry about, oh, you know, what's my public IP? Can I tunnel all the different things to get past whatever firewalls there are? So if this also works for terminal things plus Emacs client, that sounds really, really exciting. Shae: We've tried some other experiments. One of the things we tried to do was, and the only downside is like, what if my terminal has a different size, then you have to kind of shrink and match. And so we tried to honestly directly bridge to Emacs clients. And because I don't know if you're aware that there's effectively a local socket for the Emacs client that you can have multiple things connect to. But it turns out there's some sort of like system so I couldn't like reach across the network and directly use my co-workers Emacs session and he couldn't use mine. Weird things happened when we tried to do this cross host. As far as I can tell the Emacs client only works in the same host. Sacha: That's interesting. Lately, I've also been experimenting with CRDT, which has that Emacs-less plant as well. So that's been nice. But yeah, of course, a lot of people will be kind of stuck with the first challenge of finding someone that they can pair in Emacs with. Shae: I understand. And I think I'm honestly very happy that my one single coworker at this job is also a big Emacs user. And so we exchanged cool ideas and worked on stuff. And I'm very happy about that. Sacha: Were they already an Emacs person before they joined? Or did you pick the coworker because they were an Emacs person? Shae: They picked me. They were pretty much the person who started this thing. And they picked me because they'd worked with me at the previous job. Although I did have an experience like that. I had this massive Emacs config file, like 20,000 lines, and half of it was comments because it had accrued over 20 years.
Shae: And I actually really do use the calendar all the time. This is like just switch to whatever it is. Of course, my email is here. You know what, let's see... So this... I don't know, have you seen this before? Have you seen this thing called STARTED in an Org mode file? Sacha: I use a STARTED state, yes. Shae: Well, I got it from you! So if I look at like, my Org Mode configuration, a lot of this STARTED stuff I have from you, I don't know when, but you were the person who introduced me to it. Sacha: It's the reminder that I did start working on this. I tend to get distracted by intermediate tasks, so it's nice to be able to say, try to finish these ones first before you move on to the next thing, maybe? Shae: I agree. I have the same thing, yeah. And I keep meaning, because this is... I know that you can put Org Mode configuration into the first TODO item. I would really like to move it into the elisp and I just haven't gotten around to it. And it's been 10 years. I mean, maybe I should just do it.
Shae: One of the things I did that I found fun... I really have written almost zero Elisp, but I did actually puzzle my way through this a year ago. Since so much of my life is in Org Mode, I learned how to make timers. This is very close to what you get directly out of how to do timers in Emacs. After some amount of time, I want my Org agenda to pop up because I want to say like, oh, what is the stuff I'm supposed to be doing? And what am I forgetting? What has been scheduled? And what is on my to-do list? And I also like to look at what is the stuff I've been working on lately? And I really like that a lot.
Shae: Another thing that I realized is that I had a blog post that was wildly popular. Where did I put it? And it was all about Emacs. I don't know if you saw the... Here we go. It was... Ah, here it is. So here it is in... This is very much an Emacs... Sacha: Oh, yeah, I remember that one. I put it in Emacs News. I thought it was great. Shae: All right, cool. Sacha: I would like the kiddo to sometimes be able to acknowledge this, but this is not happening. Still, yes. Shae: Right, right. Yeah, and so this was really fun because, like... I had a friend who was in development and there was like millions of dollars spent on how do you detect whether a programmer is in flow and it came down to if they're typing they're probably in flow so and that was it because they tried to look at EGs and doing all kinds of other stuff but it was like if they're typing don't interrupt them. And I don't know, because I do so much in Emacs, I'm not sure how accurate this was. But basically, that's where I learned to do timers the first time. Or maybe... I don't remember which one I did first. And the idea then was as soon as basically my average typing into Emacs has gone up a certain amount, then it will actually switch to busy. And it works just fine. It was a lot of fun to write. Sacha: So yeah, interesting use of getting the activity. I've seen other fun implementations of this. I think there's a c-c-c-combo package that makes some fun animation appear if you're typing really quickly. Shae: Oh, oh, yeah. I'm guessing because I think Atom, the Atom editor had that for a while. I guess that's where it came from. Sacha: So yeah, because you can instrument Emacs and play around with it, you can certainly do all sorts of things based on that information. Okay, so you've got it, you've got it set up so that when you come back to your computer, it'll show you the stuff that you've been working on. And when you're working on the things, you can tell it to tell the rest of the world not to bug you. Gotcha. Shae: That's right. [Sacha: What other fun stuff do you have in there?
Shae: And so, at the moment, it's summer... Where did my summer go? How can this be? There we go. How come I'm in spring? Wait, isn't spring over? Hasn't summer just started? You know what I was thinking would be fun would be take the time of day, and you know that the EF themes has spring, summer, autumn, and winter, and I'm not sure if there are dark versions of each of those, but I thought, like I know that Modus themes will do this like check for the local time of when it turns dark, and then it will go from the light theme to the dark theme as soon as the sun hits, and I was like, well, what if I do that for seasons, you know, wouldn't that be cool? Sacha: There's this subtle sense of change as you go through the year. But of course you also have this thing there where you just randomize it. Shae: Well, I like that. Sometimes it's like I'm just kind of like, ah, I'm bored. I'm just bored of what I'm looking at. And so I will just change my thing. And it's just time for something. I don't know. It seems to work. It's like it gives me a little brain break from what I was staring at. And I did not know I was going to reset the effects scale, but that's fine. Interesting. What else do I have in here?
Shae: I just started doing this thing with imenu. imenu integrates nicely with LSP. Sacha: That is a very pretty sidebar thing, and I need to learn how to do that. Shae: So because I have all these extra modifiers, my s-i is lsp-ui-imenu. And the reason that what I mostly use that for is when I have like a bunch of Rust code and I want to quickly jump through the structure of it. Basically that integrates with LSP, finds all the definitions, and I can quickly jump through it. I used to use lsp-treemacs for that, but lsp-treemacs puts things in its own order, not quite the same order I want, although treemacs is quite nice. I think that the thing to do is that you and I at some time maybe the next time if we do this again we should set up with a Shwim connection and you and I can both share our Emacs and then you can show me cool things that you do and I can show you cool things that I do and then we can start filing over some of the things. How about that? Sacha: That sounds fantastic. I know we'd wanted to experiment with pair programming a long time ago so that sounds like a seamless way to do it. And therefore I will go and figure out how to install shim and get it working. I will probably need your help to actually test it. I don't know, I think I can rustle up. Maybe it'll work off my phone. You haven't tried that. But lspui, okay, so I've just been using straight up imenu, like on Neanderthal, but lsp-ui has this fancy grouping of things and colors and stuff, so I definitely want to check that out. Shae: I'm a fan, yeah. I don't know. Do I have anything else exciting that goes with this in here?
Shae: I decided that it would be great fun to do my taxes. Sacha: You are showing me your taxes, do I need to like black out this whole thing? Shae: Well, this is actually just an example from the docs. So I could actually share my taxes on it because I mostly don't care. But I think in fact you can figure out exactly how much money I'm making by looking at the open whatever. So the thing about this is that I decided to file all of my tax forms directly into Org Mode spreadsheets and then do remote lookups. So basically each spreadsheet was one particular form. And then once I'd gotten to the bottom, like I need this result, like what's my estimated income? And then I would use the lookup, kind of this cross spreadsheet lookup. And that's how I did my taxes for last year. And then my de facto mother-in-law, she's an accountant, and she didn't exactly do this thing, but it was pretty close. She was like, you've got all your taxes in the spreadsheet. I was like, yeah. And then she looked at it and she was like, what is that? And I was like, anyway. So I got to kind of file everything back out into TurboTax, but that was a fun thing to build. Sacha: Yeah, I have something like that too. So for example, whenever I do my tax paperwork, I just have to have like, you know, the step by step checklist. Okay, this is where I need to go to get this number. This is where I can put it in. And then eventually it spits out a table that says, okay, put this in box 11, put this in box 13, so that I don't have to do the steps by hand. Because even before the, you know, for me, I use like simple stacks or whatever, it's web based. But before you get to the point where you can put the numbers in the form, you gotta go to this website, calculate this thing, and Org just makes all of that so much easier. Shae: I agree. Yeah. Sacha: And this remote lookup thing is something I'm always looking up because Org tables are so powerful, but also I need more examples in my life to remember how to use them. Shae: Well, I think it took me four hours the first time to get it all figured out. But I can send you an example without showing it here. I can send you an example because I figured out, I think I've hammered the remote lookup down very thoroughly. Sacha: And once you've got it right, you can just keep filling that in or copy and paste it. You have an example of the syntax and that's already all you need. Shae: Right. I did run across some limitations of the evaluation method of Org mode spreadsheets. But maybe I've been using them a little too hard, if that makes any sense. Sacha: Oh, what kind of limitation? Shae: Honestly, I think I finally found a way to say every single... Because it was... So really the way that spreadsheets work is they're much more like Dataflow. And that is just that you end up with, like, either you work from the endpoint, which is like much more Haskell style evaluation, which is where you're like, I need to start here. What depends on this? But in the case where you have a whole bunch of different Org Mode spreadsheets, I think I ended up with this little text style hack where I just ran it a bunch of times. So it's like evaluate, evaluate, evaluate. Because remote lookups I ran, you know, I don't remember. And I think I took notes, but I don't remember. That's one of the great things about Org Mode is that I swear it's my, like, half of my brain is in my Org Mode notes. And whenever I had, I'm like, oh, what was that thing? I'm like, well, fortunately, with my terrible short-term memory, I took copious notes because otherwise I would never be able to get back to it.
Sacha: So Org timer is a separate thing. It's useful for meetings and things like that. You would say, okay, your Org timer starts at the beginning of the meeting and then you can have a list and it automatically, like if you alt shift enter or something like that in the list, it'll automatically like insert the right timer, relative timer to it. There you go. So there's an org-timer-start. But the reason I didn't go that approach was because then you A. have to remember to actually start the timer and B. then you have to synchronize your time with video time. Which might not have started at the same time. So now I'm just like, okay, wall clock for everything. And then I can do the transformation with whatever I like. And since I'm editing my subtitles in Emacs, I can say, hey, this file started at this time, according to YouTube. And then just, you know, map all of the wall clocks to the appropriate subtitle times. Shae: Wow. That's really cool. Sacha: Anyway, so timers, relative, absolute, and using abbreviations is great. Which I think actually is a thing that I picked up from Karl. Karl Voit because he also likes to use... He has an abbreviation, not at the Emacs level, but he has an abbreviation on his system level, like with his window manager, so he can use this timestamp trick anywhere, including in Etherpad or wherever else where you want to insert the date and time. That's V-o-i-t, by the way. But yeah, so times are a great way to just leave yourself a pointer to that moment so you can go back to it later. Shae: Now I'm curious, how well does that integrate with this sort of thing? Because I really like looking back at my history agenda. Sacha: If you have it insert an inactive timestamp, I think it should still show up there. I think it will be a little like those. Shae: Yeah, it looks like the... Well, it looks like these two are showing up. Sacha: Yeah, yeah, yeah. Yeah, so that's a basic thing that I would have inserted by my either abbrev or... So it's not even dabbrev. It's just regular abbrev in Emacs. Shae: What's the difference? Sacha: dabbrev is like hippie... Okay, let me just double check here. I feel like dabbrev is sort of hippie expand-ish. It looks in your buffer or possibly other buffers. And I think hippie-expand and dabbrev, they kind of work together. It's an option to have them work together. Okay, so hippie-expand is... Oh, so I see. Hippie-expand is the more advanced version of dabbrev. dabbrev was Dynamic Expand, and Hippie Expand says, yes, that, but try a whole bunch of other things first. But my timestamp thing is actually just done by a regular abbrev, and I will find the thing in my config for "ot". Oh, yeah. I will put it in my chat. Shae: My spelling, most people say my emails are spelled really well, but it's only because I have ispell set up. Sacha: Yeah, ispell is great. I am learning French and therefore... Shae: Oh, c'est très bien. Je parle un peu de français aussi. Sacha: Oh, oui. I'm keeping a journal in French on my blog and I have the Tatoeba Project with all the example sentences and I have a consult interface to look up stuff in them so I can just borrow other people's words and try to make it sound more natural. Plus of course the usual searching for words in dictionaries and stuff. Anyway, in the chat, I put in my global abbrev table definition for insert format time string. In case you want to steal that, it's right there. Shae: I will definitely save that into my notes here.
Shae: Oh, I've got a cool thing that I'm doing for work. And that is that our readme file is not only a word file, but we also have the demonstration of our actual thing is done by using like dependent snippets. And so that means that like if you want that, perhaps this is something everyone already knows, I don't know, but we basically are using the results of earlier commands in later places. And the other nice thing about that is that then when we want to check, we have to effectively dock tests, right? When we want to check and see if our software works the way it does in the readme, we evaluate the final Org Mode snippet, which then calls it forward, calls it forward, and then if something goes up or not. Well, I guess I need to fix something. And so it was pretty exciting to put Org Mode niftyness into our, into my Word reading file, you know? Sacha: Nice, nice. And you did mention your other coworker is on board with the whole Emacs thing. So that's one of the things that people are often like, I want to use Org Mode and I want to use it for like the documentation or the testing or whatever, but they got to get everyone else on board with the thing. Otherwise it's Jupyter Notebooks or whatever else, right? Shae: Right. Okay, so I have a joke for you that I came up with a long time ago, and that is, do you know the only way, there's only one way that Sauron could have organized the invasion of Middle-earth, and do you know what he used? Sacha: What? Shae: Orc Mode. It's a terrible joke, isn't it? Sacha: That's okay. I'm sure someone in the comments will come up with an even worse pun. Shae: I'm excited! It's going to be great! Sacha: Never underestimate the punniness of the Emacs community. Shae: I completely agree. I don't know. Do I have anything else exciting in here?
Shae: I actually really like this one. I used to run all of my tests in compile. F12, I have F12 bound to compile. And one of the things I wanted was, I wanted something where it was, if the compile is successful, don't show me the results, because everything's good. And so since I'm doing stuff in Rust, when I run all the tests, it leaves the buffer up, and I need to get around to actually doing stuff like this for Rustic mode as well, where when the tests pass, just go away, because it's all good. And when the tests don't pass, show me where to... I need to look at the problem. And I got this from Enberg and Emacs, I don't know, 20 years ago. Maybe it was less than 20 years ago, but it probably wasn't. So yeah, there's so much good stuff. Yeah, there's just so much good stuff. And I also like to, oh, look, here we go. You can see that this is long gone, by the way. It's not there anymore. Sacha: I have a proper, you know, it's sachachua.com/dotemacs. A lot easier to remember. But yeah, and I think that's, yeah, yeah, I remember that now. defadvice is also obsolete. The new hotness is advice-add or something like that. Shae: Oh, really? I'm going to make another TODO item for there. Sacha: I was digging through my notes trying to find, do you share your config anywhere? Shae: No, but you know, at this point if I share it on YouTube, I might as well just throw it up somewhere. Why not? It's not very exciting. Like if you look at someone like Ross Baker who has magic, like wow, is there some magic coming in from Ross Baker? I'm so excited to see more stuff from him. There's just like, I guess I feel like compared to almost everybody else I know, I feel like a power user. Because I'm like, you know, I wish I could do this thing. A lot of times someone I know is like, well, I did that thing and here's a library. And I'm like, yeah, I'll have to do it. And I just, I guess I feel like I'm a power user. And on the good side, I guess I kind of, I really haven't written that much Elisp ever, like I was saying in the comments during your interview with Prot. And I kind of like to, it's just I guess it's never quite gotten to the top of my stack. And I did decide it was time for me to send money to Parade for at least for themes, if not for like, please teach me some Elisp so I can actually, because you know, it's not that Elisp is hard. It's more like, how do I kind of, what are the things I interact with? What are the words? What's the vocabulary of working with Emacs? I don't actually really know. As a user, sure, I can do cool stuff. I can do Lisp macros. I've done Scheme and Lisp some of the past, but not inside Emacs. Sacha: Alright, so let me clarify. After more than 20 years of using Emacs, did you say you feel like a power user or do not feel like a power user? Shae: I definitely feel like a power user, but I don't feel like someone who does much of anything with Elisp. I don't really feel like someone who has much of a clue in the internals. And that's not entirely true. I have some of the ideas. But for the most part, I haven't actually needed to know that much about the internals. And sure, I've dug into things like how do you efficiently work with large buffers in your ??, like the ropes data structure and stuff like that. That was more for fun. Although it is something that Emacs does and does extremely well. But I'd kind of like to... There's a lot of things I'd kind of like to change and I don't really have enough of the understanding of the kind of how I would write the Elisp to do it. Here's a good example. When I hit F3, it takes me to the one I'm currently clocked into. Unless I haven't clocked in to something since I started Emacs. And honestly, I would like to use something like org-ql, the Org query language, to go find if I've just started Emacs, and Org does not know about something, you know, I just want you to go search for it. I have so many cores and so much memory, just go find it. Sacha: That sounds like an excellent reason to go learn Emacs so that you can have it... If you're not currently clocked in, go find the most recent clocked in task and go there, or maybe present you with a list of things and then go from there. I would love to hear about your Emacs Lisp learning journey because that's one of the big things that moves people from, you know, power users, yes, but users, to using Emacs as a lightweight editor toolkit for something that's custom fit to exactly what their workflow is. And on that note, I'm going to try to wrap up gracefully before the kiddo, you know, just like drags me out here. Thank you so much for doing this. I look forward to more conversations. I'm going to post the transcript and other things like that pretty quickly, I think, because I have this nice workflow now that lets me take screenshots and everything, but there's so much here that I want to unpack. But I hear the kiddo, bye!#+begin_export 11ty
<a name="end-ec22-transcript"></a></details> #+end_exportbvt
Find more Emacs Chats or join the fun: https://sachachua.com/emacs-chat
You can e-mail me at sacha@sachachua.com.
-1:-- Emacs Chat 22: Shae Erisson (Post Sacha Chua)--L0--C0--2026-05-07T18:55:38.000Z
Figure 1: JPEG produced with DALL-E 4o
My dotfiles for my MacOS rice and Emacs configuration live in two public repositories. Both repos are shared as a reference; clone, fork, or just lift the bits that look useful to you!
This post is a thin entry point, and the READMEs in each repo carry the actual detail.
A single bootstrap.sh that takes a clean macOS install to a fully provisioned development machine in roughly thirty minutes. It installs Xcode CLI tools, Homebrew, and a long list of CLI utilities and language toolchains, then symlinks every config in files/ into the matching path under $HOME.
See repo for installation instructions.
What gets installed:
gh, k9s, bat, fzf, ripgrep, eza, jq, lazygit, AWS CLI v2, and more
The full package list lives in the repo's Brewfile. bootstrap.sh also clones .zetta.d to ~/.zetta.d as part of the Emacs setup; if you only want the shell side, comment out the emacs section.
My Emacs configuration, packaged as a small distribution. Around 320 packages wired up via a module DSL, with an Elpaca lockfile pinning every package to an exact commit, and byte- and native-compilation done up front so the first launch is clean.
The name is a cheeky play on how we name certain minimalist text editors after Metric Prefixes nano (10^-9) or pico (10^-12). This maximalist editor config is named after zetta (10^21).
See repo for installation instructions.
Notable parts:
zetta-modules! in ~/.zetta.elZetta also hosts several small packages I've written that live in their own public repos. See the README's "Bundled custom packages" section for the list. None of these are released or publicized yet, so bring a pinch of salt if you choose to try them.
-1:-- My Dotfiles: macOS Bootstrap and an Emacs Distribution (Post Charlie Holland)--L0--C0--2026-05-07T15:18:00.000Z
Jonathan Chu is a software engineer and Emacs enthusiast who’s been looking for a note taking app that he can actually use. He’s tried all the usual suspects but found them too opinionated for his liking. He wanted to work in Emacs and write his notes in Org mode but none of the existing packages were quite right. So he did what Emacs users always do when things don’t work exactly as they like: he wrote an Emacs package do things the way he wanted. The result was grove.
Chu says that Grove was informed by his experience with Obsidian. I’ve never used Obsidian so I can’t comment on that aspect but Chu says that Grove is Obsidian-like. The only external dependencies are ripgrep, Graphviz if you want the graph view, and Consult if you want the enhanced search capability. One of his goals was to have an easily installable and configurable package.
It looks like a pretty nice package if you’re looking for a note taking method with a bit of but not too much structure. I use Org mode for all my note taking across many dimensions and have never felt the need for a more organized approach. Sure, sometimes I think about using Denote or Org Roam but—for me—they never seemed worth the effort. Many others disagree, of course, but Emacs has a home for us all,
Take a look at his post or the Github repository for the details and a bit more on how things work.
-1:-- Grove (Post Irreal)--L0--C0--2026-05-07T14:58:24.000Z
I've released an update to
blogmore.el, my
Emacs package that helps me out when writing this blog. I've
added two commands to this version which help me be lazier than ever.
The first is blogmore-become-like. When run, this prompts for another post
and, once selected, it sets this post's category and tags to be the same
as the other one. I added this because I'm often writing an occasional
series of posts that are all about the same project, and so I always find
myself copying and pasting those frontmatter properties from another post.
The second command I've added is blogmore-toggle-image-centre. Built into
BlogMore is a little bit of styling that will
ensure an image is placed in the centre of the page, if the URL for the
image has #centre on the end. This means that, for most images I add, I
have to go and edit the URL to add that. Now I can just run a single command
when the cursor is on an image and it'll add (or remove, if it's already
there) that styling hint.
In both cases, I've added the commands to the transient menu too.
-1:-- blogmore.el v4.4.0 (Post Dave Pearson)--L0--C0--2026-05-07T08:05:10.000Z
capture icon
Product: Capture
Price: $7.99 · lifetime updates · up to 5 Macs · 7-day free trial
Capture is a product designed to capture something, and move on with your life. It is an universal product, you can be in any application, Safari, Chrome, BBEdit, Finder, anywhere. You can press the keyboard shortcut to invoke capture. Fill in what you want to capture. Hit return and go back to whatever you were doing.
The product is advertised as “Capture thoughts to Obsidian without leaving what you’re doing.” You don’t have to use Obsidian. I use Emacs and it works great for that too. You can use any text editor and it is as useful.
capture options
Capture gives you two options to capture your content.
capture where?
capture and folders
You can define the Obsidian vault that Capture deals with. In my case it is the folder which contain my org files. Within that folder is another folder called ‘capture’ where I put my captured files into.
capture context
Within these options you have various options. You can include context. You get to define the context.
capture tags
You can add tags to your captured content.
I am happy with Capture. It is a focused utility which does what it promises well.
⌘↵. On my machine ↵ saves the capture. There is no way to add a new line. This must be a bug.I used the program for fifteen minutes and bought it. It is the kind of utility which I am going to use multiple times a day.
Recommended most heartily.
macosxguru at the gmail thingie.
-1:-- Capture and Move On (Post Bicycle for Your Mind)--L0--C0--2026-05-07T07:00:00.000Z
twtxt was one of the strongest inspirations behind Org Social: a plain text file served over HTTP, with no active server, no database, no signup. I personally admire the work done by Buckket: the concept is so powerful that it has captured the hearts of many people over the years. Its author gave us an elegant solution for creating a personal microblog, an alternative to Twitter (now X).
That said, Org Social would not exist if it weren't for the limitations of twtxt or the natural evolution of social networks. Org Social keeps its spirit, learns from its mistakes and from the competition, reinforces other concepts (like federation) and adopts modern features (like visibility). It is not an improvement, it is a shift in approach.
That's why I'd like to go over the three most important aspects where Org Social improves on twtxt.
The most obvious one is the ability to have structured metadata thanks to Org Mode.
#+TITLE: Bob's journal
#+NICK: Bob
#+DESCRIPTION: I'm a software developer and I love open source.
#+AVATAR: https://my-awesome-website.com/avatar.jpg
#+LINK: https://my-awesome-website.com
#+FOLLOW: https://foo.org/social.org
#+FOLLOW: https://jane.com/social.org
* Posts
** 2024-12-12T12:00:00+0100
:PROPERTIES:
:LANG: en
:TAGS: emacs org-social
:CLIENT: org-social.el
:VISIBILITY: public
:MOOD: 😊
:END:
Hello Org Social!
This opens the door to giving context to each post, having native threads, tagging, organizing in groups, defining visibility, running polls...
| Feature | twtxt | Org Social |
|---|---|---|
| Format | plain text (Markdown optional) | Org Mode |
| Mentions | yes | yes |
| Profile metadata | minimal | rich |
| Tags | #hashtag inside the body |
:TAGS: as metadata |
| Native threads | no | REPLY_TO |
| Multiline | no | yes |
| Sub-headers | no | yes (#+) |
| Federated groups | no | yes (GROUP) |
| Languages | no | yes (LANGUAGE) |
| Polls | no | yes |
| Reactions / boosts | no | yes |
| Visibility | no | VISIBILITY (though not fully private) |
| Account migration | no | MIGRATION |
We don't just get richer post bodies: the possibilities for interaction between users grow without limit. The specification invites you to create, not to work around limitations.
Org Social follows these principles:
Without clear rules, the ecosystem fragments. There isn't a single line of the specification that isn't aligned with the points above.
A very important element in Org Social are the Relays, which let you organize threads, discover other users, receive notifications, run searches, get an RSS feed, etc. However, without them the network would still work: users could keep publishing, following others and interacting. Relays are not the core of the social network. Clients can rely on them, but should never depend on them. This isn't a quality exclusive to Org Social: twtxt is also aligned with this principle. The point lies elsewhere.
The goal of clients is to make reading and writing feeds (social.org) easier, not to be a requirement to participate. That said, they are very practical for building your timeline, since you need to read and sort the feeds of the users you follow. For that there are Desktop and Android versions thanks to Emacs, plus a native iOS version written in Swift.
The differentiating point is balancing different user profiles via the infrastructure. For example:
social.org on GitHub, GitLab, their own server, etc. They control and manage where it is hosted and how it connects.It embraces every kind of user, without sacrificing the essence of the social network. You don't need to be an expert to take part, but if you are one, you control every byte.
Org Social doesn't just improve on twtxt technically: it also evolves the concept of a social network on top of plain text. It takes a step forward by providing more infrastructure and incorporating modern features.
The future is not about going massive or competing with Mastodon, but about satisfying a very small niche of users.
Focus on the content or on engaging with others, Org Social will take care of the rest.
-1:-- twtxt vs Org Social: the evolution of an idea (Post Andros Fenollosa)--L0--C0--2026-05-06T13:21:31.000Z
Yesterday I joined Sacha Chua for a new episode of her Emacs Chat
podcast, where we talked about Emacs and life. I gave a quick tour
of my Emacs configuration, discussing at length my configurations for
EXWM (Emacs X Window Manager) among other topics like Emacs's facility
for visually indicating buffer boundaries in the fringe by setting
indicate-buffer-boundaries and my convenience configuration macros.
The above video is provided with closed captions and the below transcript courtesy of Sacha with minor fixes and formatting by me. I've included some of Sacha's screenshots from our chat, you can see the rest on the episode's page on Sacha's blog.
A few links from our chat:
ffs (Form Feed Slides) package for simple presentationsIt was a lot of fun - thanks again for having me, Sacha!
Take care, and so long for now.
For the full transcript please see: https://kelar.org/~bandali/gnu/emacs/emacs-chat-202605.html
-1:-- Emacs Chat with Sacha Chua (Post Amin Bandali)--L0--C0--2026-05-05T23:43:44.000Z
Figure 1: JPEG produced with DALL-E 3
This is the fifth post in my series on Emacs completion. The first, Incremental Completing Read (ICR), explains what modern completion actually is, and how Emacs exposes it as a programmable substrate rather than a closed UI. The second introduced the VOMPECCC stack of eight packages covering the six orthogonal concerns of a complete completion system. The third toured spot, a Spotify client built as a thin shim on top of those packages. And the fourth built a produce picker from scratch, demonstrating the specific features that each VOMPECCC package provides.
This post is the practical complement to all the other posts. Here, we showcase over a dozen workflows I use every day. Most are powered entirely by features that ship in the box with the VOMPECCC packages, and there are 'Bonuses' which demonstrate workflows enable by 3rd party packages that build on top of VOMPECCC. The prose is deliberately thin, and you will find most of the demonstration is in the video below.
As in the previous posts in this series, the upper-right of my Emacs (in the tab-bar) shows the keybindings and command names I am invoking, so you can map what you see onto your own configuration.
Two configuration choices show up repeatedly and are worth naming once upfront so the keystrokes are intelligible.
Async split character. My consult-async-split-style is comma, not the default #. In Consult commands like consult-ripgrep, everything before the first , is sent to the external tool as the search pattern, and everything after is filtered locally with my completion style.
Orderless dispatchers. My orderless-style-dispatchers bind affix characters to matching styles: @ for Marginalia-annotation matching, ~ for flex, ` for initialism, ! for negation. Each can be a prefix or suffix on a component. My orderless-component-separator is also ,, so a single comma serves double duty depending on context.
consult-ripgrep → input your search term → embark-export → wgrep-change-to-wgrep-mode → edit as you like → C-<return>
consult-ripgrep with , splitting external (ripgrep) from local (Orderless): for example, error,handler,~retry,!test.
consult-buffer with narrowing keys: b SPC for buffers, f SPC for recent files, m SPC for bookmarks, p SPC for project items.
consult-line within the current buffer; consult-line-multi across all buffers (or the project, with a prefix argument).
consult-imenu within the current buffer; consult-imenu-multi across every buffer of the same major mode.
consult-info for Info manuals (Emacs, Elisp, Org, plus every package that ships its own .info file); consult-man for system man pages.
M-x window @frame. The @ dispatcher routes a component through orderless-annotation to match against Marginalia's docstring text rather than the candidate name. This lets you query for commands by what they do rather than what they are called.
C-> (embark-act-all) runs a single Embark action on every candidate currently surviving in the prompt.
You can also embark-select to create a subset of displayed candidates and use embark-act-all to act on only those selected candidates.
embark-become switches the active command (e.g. find-file → switch-to-buffer) without losing the input I have already typed.
xref-find-references (M-?, with xref-show-xrefs-function set to consult-xref) → embark-export → wgrep-change-to-wgrep-mode → edit → C-<return> to write your changes. This is very similar to the ripgrep version above but driven by the language server, so foo the variable and foo the unrelated comment stay separate.
consult-recent-file → embark-export produces a Dired buffer, putting every Dired operation (mark, copy, rename, chmod, batch shell command) on the recent-files set.
In any Vertico session, C-' jumps to a labeled candidate (Avy-style); C-" does the same jump and hands the candidate to Embark.
s-V (vertico-repeat) reopens the last completion session with its prompt, input, and selected candidate intact.
consult-ls-git surfaces working-copy status, tracked files, and branches in a single multi-source prompt with narrowing keys. A nice on-the-fly alternative to the Magit status buffer.
consult-gh-search-repos streams GitHub repos as candidates; C-= previews the README; M-S (vertico-suspend) (or simply moving your cursor out of the minibuffer) detaches the minibuffer for free reading; s-V (vertico-repeat) (or simply moving your cursor back into the minibuffer) resumes; C-. exposes Embark actions (clone, browse, view issues, view PRs, view files, fork).
consult-gh-search-code against the contents of every public repository on GitHub. You get the same VOMPECCC features, but with the search space expanded to "all open source code in the world".
consult-omni-web fans one query out across Google, Brave, Wikipedia, StackOverflow, YouTube, and a gptel-backed LLM source simultaneously; s-j / s-k jumps between source groups; C-= previews; C-. surfaces Embark alternates (open in EWW, copy URL, etc.).
What makes this so cool is that none of these workflows required a single line of custom code. Each is built entirely out of the features that ship with one or more of the VOMPECCC packages. Pick the two or three that map onto frictions you already feel, and the rest will reveal themselves ad-hoc as you encounter new frictions.
Over a dozen high-impact Emacs workflows are demonstrated in this post: multi-file refactor, two-stage ripgrep, unified buffer switching, line search with preview, symbol navigation, docs search, M-x by docstring, batch action, mid-prompt pivot, symbol-aware refactor, recent files as Dired, and quick jump + act, session resume. Each of these workflows is composed entirely from features that ship in the box with the VOMPECCC packages.
-1:-- Emacs Completion Showcase with VOMPECCC (video) (Post Charlie Holland)--L0--C0--2026-05-05T18:15:00.000Z
I understand the gravitational pull of Emacs though for me it's been Vim/Neovim. I've spent entire year trying out tools like VSCode and Atom and sure they had some features that were neat, but they were never Neovim.
It's been at least 5 years since I tried another editor for coding though after realizing that I'd just go back to Neovim, which brings me to my Emacs adventures. I tried nvim-orgmode and it sort of worked but had issues that I was never able to work through. Then I went back to Todoist for a bit and missed orgmode, even if it was a hobbled form in Neovim.
This led me to Emacs and orgmode which I've been loving for about a year now. Still I keep wondering if I should move my coding workflow to Emacs and abandon Neovim?
There is a pull to just use one tool.
-1:-- The gravitational pull of familiar tools (Post Curtis McHale)--L0--C0--2026-05-05T16:26:00.000Z
This month's carnival topic is "may I recommend...". I'm too new to Emacs to recommend some fancy package or an interesting workflow so I'll reach back to my 20+ years doing software and finding tools.
May I recommend....keep it simple. Don't grab 50 new things from some video or blog post that some creator put up. People do that because it looks cool and then they over complicate their systems and abandon them. They layer complexity that looks cool over a system that already is working for them to solve problems they don't have with solutions often produced by people whose job is to get a new piece of content out that will go viral and pay their bills.
Many times it doesn't even seem like the creator uses the system at all, it just looked cool and had the proper keywords for what's popular right now.
I say, identify a problem you actually have. Spend some time researching options. Try out a few of them to see what works. Then stop messing around with your system and get work done.
-1:-- Emacs Carnival: May I recommend... (Post Curtis McHale)--L0--C0--2026-05-05T16:11:00.000Z
Protesilaos (Prot) has an excellent post on keyboard ergonomics for Emacs users. He considers various strategies for making Emacs commands easier to use and less likely to cause RSI damage. He considers everything from split keyboards to evil-mode.
He says he has both a split keyboard and a keyboard with a normal form factor. He says that the split keyboard is not a magic solution and that what works best for him is to configure “one shot modifiers” where the modifier is simply pressed—but not held down—before the key it modifies. So, to use his example, Ctrl+x is typed by tapping the Ctrl key and then the x key.
Along with that he suggests ordering the modifier keys in a way you find comfortable. The most important thing, he says, is to keep the Ctrl key close to the space bar so that you can use your thumb to press it. It’s also important to have the modifier keys located symmetrically so that, for example, you can use either hand for the Ctrl key.
As for the common advice to map Caps Lock to Ctrl, Prot says that’s okay but that it encourages the bad habit of always using your left pinky for Ctrl.
There’s a lot more information in Prot’s post—including how to configure one shot modifiers—so be sure to take a look at it.
I’ve been incredibly lucky. I use a standard keyboard, map Caps Lock to Ctrl and don’t have any symmetric modifier keys. Nonetheless, I haven’t suffered any RSI problems despite spending the majority of my day at the keyboard. But, as I say, I’m lucky. The smart thing to do is to read and head Prot’s advice.
-1:-- Emacs Keyboard Ergonomics (Post Irreal)--L0--C0--2026-05-05T16:05:03.000Z
I almost always reach for project-vc-dir when I want a VC status overview, and most of the time this is exactly what I want, the whole project laid out in one buffer, every modified, added and unregistered file in the repo sitting right there, ready to be diffed or committed. But every so often, particularly when I am deep inside a big repository and I only really care about a single subdirectory's worth of changes, that project-wide view is, frankly, a bit too much. Too many rows, too much scrolling, too much noise.
So, what am I actually after?, I want the same vc-dir buffer, but scoped to whatever directory I happen to be looking at, most commonly the directory I have open in dired. And it turns out this is almost trivially easy in vanilla Emacs.
The bit I had not initially appreciated is that vc-dir itself already accepts a directory argument, it is the interactive prompt that steers you towards the repo root, because it defaults to (vc-root-dir) rather than default-directory. If you call it non-interactively with a subdirectory instead, the Git backend quite happily scopes the status listing to files underneath that path, even though the overall VC root is still the same project root.
So the fix is a tiny wrapper that just hands vc-dir the current default-directory and skips the prompt entirely:
(defun my/vc-dir-here () "Run vc-dir on the current directory (dired's dir when called from dired)." (interactive) (vc-dir default-directory))
Then, because the main place I actually want this is from within dired, a keybinding that sits nicely alongside the standard C-x v family:
(with-eval-after-load 'dired (define-key dired-mode-map (kbd "C-x v D") #'my/vc-dir-here))
The mnemonic, such as it is, is that C-x v d is the normal vc-dir binding with its usual prompt, and capital D is the "here, right now, this directory" variant. Lowercase for the prompted version, uppercase for the zoomed-in one, which also pairs up nicely in my head with project-vc-dir being the zoomed-out project-wide thing on C-x p v.
So the little two-tier workflow I have settled into is:
C-x p v – project-vc-dir, show me everything in the projectC-x v D from dired – my/vc-dir-here, show me just this subdirectory
And that is really the whole post, nothing clever, no new package, just a three-line wrapper and one keybinding, but it has genuinely taken a surprising amount of friction out of navigating VC state in large repos where I know perfectly well the only thing I have touched is under src/foo/, and I do not particularly want to be reminded of every other outstanding change elsewhere in the tree.
And yes, magit can do this with some narrowing, but actually, I like vc-mode!
-1:-- A Zoomed in vc dir for the Current Directory in dired (Post James Dyer)--L0--C0--2026-05-05T06:50:00.000Z
I've moved the repositories of the geiser packages I maintain to codeberg, so make sure you update your remotes if you access them directly through git. The URLs are already updated in the ELPAs, so nothing changed if you installed them via their packages.
Seventeen years and running. Happy hacking!
-1:-- geiser moved to codeberg (Post jao)--L0--C0--2026-05-04T19:09:00.000Z
I chatted with Amin Bandali about Emacs, configuration, EXWM, keybindings, audio, and life.
View it via the Internet Archive, watch/comment on YouTube, read the transcript online, download the transcript, or e-mail me your thoughts!
Links:
Amin: So that's a brief introduction, and then I have an early init section for doing the early init file. There's a couple of subheadings here. Actually, let me enlarge the font size a little bit to make it more legible. OK, great. I do a couple of things here like disabling package at startup because I don't use package as I mentioned. I manually install and update my packages as git submodules in my configurations repository.
Amin: I set load-prefer-newer to t to make sure that I never load any stale code. For example, I might edit some Emacs Lisp file by hand and forget to byte compile or native compile it. And this tells Emacs to basically just use the version of these three variants that's the most recent. Yeah. Nothing super fancy here.
Amin: I turn off a couple of things that I find a little bit distracting, like the menu bar or toolbar. Although I do say here that for people who are new to Emacs, they're actually super helpful. Sure, it's a little bit of visual clutter, but in the beginning, it's really, really helpful to help you orient yourself of what mode you're in, what tools do you have available in your disposal. And even someone who's been using Emacs for more than 10 years, I also use it sometimes when I'm like… just starting to use a new mode. So yeah, good stuff.
Sacha: Yeah, and then you've got a whole bunch of things where you set some variables to nil temporarily to make it faster, so that's in your startup in garbage collection. Amin: Exactly. Empirically, there is no hard and fast science behind this. I experimented over the years. I'm pretty sure I believe the default, for example, the garbage collection cons threshold is about eight megabytes. I tried increasing that a little bit to see how much If I increase it to what point will it make my startup faster? And I found this 30 megabytes or mibibytes to be kind of a sweet spot. So I bump that up. And then after Emacs has finished initializing, in the after-init-hook, I just restore the defaults.
Amin: And then, yeah, this is the bit with the user-lisp-directory that I was talking about. Awesome stuff. So you can basically designate a directory. For example, in my configuration, it's just a lisp directory. And then on startup, Emacs will go through and byte-compile, native-compile if necessary, and then add all of that stuff to the load-path automatically. So you get that. Yeah, and then this is the bit about site-lisp that I was talking about. So if you want to use user-lisp, but you're still using older Emacs versions that you maintain, you need to maintain backward compatibility in your config. This is how you do it, for example. So you just yeah, add it to load-path, require it and then call prepare-user-lisp. That's about it.
Amin: Yeah, and then I have the main init file. And there's not much in it. It's just the debug-on-error and debug-on-quit. So the debug-on-error thing, I set it to the value of init-file-debug. And if you look at that, the help for this variable, basically if you pass or launch Emacs with --debug-init, this variable will be true. So yeah. Sacha: I did not know that. Cool. Amin: Yeah, it's pretty helpful. I think, if I'm not mistaken, I took this from John Wiegley's dotemacs, but I can't remember for sure. It's been years. Yeah, it's pretty nice. And then here, I just set my name and email address. And very early I set a custom-file to keep all of that stuff separate from my .emacs. I don't want it mixing in.
Amin: And then pretty much the only other thing that's in my main init file is just to require and load these different modules or packages of my configuration. I have these as actual packages or as actual features. They provide themselves. And that's just something that I've found straightforward enough to do. I know, for example, Prot uses a dual approach. He has some of his configuration that's more readily usable, available as actual packages. And then the other ones, it's just Emacs Lisp code. It's not actual packages. But for me, I just keep it simple. Everything as packages and that's about that. Sacha: Fantastic. Let's dive into some of those configuration modules. Amin: Sure, let's see. Yeah, so this there's this like core thing which is kind of included gets included in all of my other files.
Amin: I wrote a bandali-configure macro shamelessly based on prot-emacs-configure which is what Prot uses and it basically is a way of kind of similar to use-package for like wrapping a bunch of relevant like Emacs Lisp code all together. It has the benefit, if you use it, if there is an error in that block or in the body basically, then it won't crash everything. That body will just get ignored and we display an error. And that's also the main reason that Prot uses it. The one thing that I added extra to mine, which I took with inspiration from Eshel Yaron's esy/init-step, is to wrap it up in basically time the execution of each of these blocks, which can be pretty helpful to help you see, okay, which part of my configuration is particularly slow. Usage examples. I just have it here. You can either basically pass it like a symbol like thing or you can also pass in a string as the first argument. And this is what will be displayed when you display a list of the evaluation times for all of these blocks in your configuration.
Amin: Yeah, and then I have a neat little function here like bandali-configure-report-times that will report these times, whether in the order that it's encountered them, or you can have it sort by fastest to slowest, slowest to fastest, blah blah blah. Sacha: You mentioned you're no longer using this. Is it because you wanted it to be easier to copy and paste your code? What got you to shift back to the regular vanilla type of configuration? Amin: Right, as neat as it is, I didn't find it super useful. For one thing, because I don't add or remove a ton of stuff to my Emacs configuration regularly, so if there is an error, it wouldn't cause an issue for the rest of my configuration. I didn't really find that very useful. And then my other potential concern is that the way I was structuring things, I would put all of the configuration, let's say for Gnus, in one of these blocks. But I wanted to be able to break that down into, for example, Org Mode sections more easily. So far, I just decided to not use it. I know I could technically break those down into smaller blocks, but I haven't done that yet. Sacha: Ihor says, this configure macro looks a lot like good old use-package, which you're not even using in the rest of your config. And I hear you about wanting to be able to split things into smaller blocks with more explanations in between them. So in my config, yeah, sure, I've got the use-package there to do the ensure and all that stuff. But I also have with-eval-after-load because I still want, you know, the links and the screenshots in between.
Amin: Then I just have another quick macro thingy here, bandali-define-keys, which wraps around Emacs's define-key. It affords me the convenience of defining multiple key bindings, and Prot's version of this (I think it's prot-emacs-keybind, or something like that) he imposes the limitation that the keys should be valid strings that can be passed to the kbd function, which is very fair and valid, but I wanted to not impose that, to keep the flexibility of using define-key directly. The consequences of that, as we can see, is we can pass in the old representation of key bindings, like the vector or whatever syntax, which Prot's doesn't support by choice, whereas mine does. Let's see. For example, let's look at the bandali-theme.el, which is all about… The appearance, I guess, of Emacs.
Amin: Yeah, so I just have a conditional block where, you know, if you're in a graphical environment, I'll just go ahead and load Prot's doric-themes, specifically doric-oak, which is what we're seeing right now. I'm using, it's very beautiful, it's very subtle, and it uses emphasis, bolding and stuff to draw your eye to something instead of using a million different colours, which I find pretty nice. Yeah, and then for example here I set up some fonts. I use this Sahel font for Persian and Arabic text. I set a colour emoji font here and this is like we get a kind of preview of what I do. It's like with-eval-after-load 'faces and then blah blah blah. Sacha: Ihor would like to point out that with-eval-after-load is also a macro that calls another macro. So I'm just going to mention it because it's there. These are your fonts. This is your theme. This is great because everyone always asks, what theme is this? What font is this? All right.
Sacha: I like your text scaling tweaks that you're just about to go into. You've changed the global mappings. Amin: Yeah, yeah, yeah. And I actually took this from Prot as well. And it makes a lot more sense. So by default, this, C-x C-+, -, blah, blah, blah, it only scales the text for the current buffer only. But in newer versions of Emacs, in Emacs 29, they also added commands to adjust this globally, including the mode line and all that stuff, which is usually what I want, for example, in this presentation or when I'm sharing my screen right now. It scales everything up globally. So yeah, I just swapped these to be the default, and then I add keybinds for the just local variants in case I need to use that. Yep.
Amin: Start the Emacs server if it's not running. And this is very useful, very helpful so that then you can call into an existing Emacs process with emacsclient and have it edit a file. I don't use it for anything fancy just yet. I believe Prot also mentioned in his video with you, Sacha, that he uses it for things like org-capture to spawn a new buffer in his existing Emacs session and things like that. You can do pretty cool things with it. But yeah, I just use it for being able to easily use my Emacs as EDITOR and VISUAL text editors. So yeah, this sets that up.
Amin: Adding a fundamental mode hook. Again, I took this from Prot. Sacha: I was surprised by that because I was like, oh, there isn't a fundamental-mode-hook? Okay, that makes sense now. Amin: Right, right. Yeah, there isn't a fundamental-mode-hook by design. But I still, in the past, have found that I wanted that. For example, for this display-fill-column-indicator, when I had it enabled everywhere, I was like, it would be nice if I could at least disable it for Fundamental mode. And at the time, I didn't have this. I added this just recently. So if I decide to go back to using something globally, but I don't want it in fundamental-mode, then I can disable it using this. Yeah, and then some standard stuff like I prefer spaces and a tab width of four characters.
Amin: Visually indicate buffer boundaries. This is a little bit hard to see right now, but here at the bottom left
Amin: you see a little down arrow
Amin: and then the little top arrow. And… Let's see if I can. Sacha: Oh!
Amin: And also here, for example, when it all fits in the view. Sacha: Huh, that is cool. I was looking at that. What does it do? And so that tells you, you can still scroll up or you can still scroll down, and you don't have to look at the scroll bar to see where you are. It just says there's more there. Amin: Yeah, exactly. Yeah. And it also helps distinguish when there's a newline character at the end of the file or not. So here in this buffer, there is.
Amin: But if I delete that, you see this indicator here changed shape. But if I go back and add the new line again. So yeah, that's also been very helpful for me because I added configuration files and some of these pieces of software are sensitive to having a new line at the end of the file. So yeah, it's very helpful and useful for that. Sacha: I would not have guessed that from the very short line in your config that turns that on. It's one line, (setq-default indicate-buffer-boundaries 'left), and yet it adds this nice little nuance to the way that fringe looks. Amin: Right. Yeah, absolutely. Perhaps I should expand more on it at some point later to explain these things. But yeah, just this one line. Sacha: May I recommend screenshots? Amin: Yes, you may, for sure. Yeah, I will definitely do that as well, because I'm also a bit of a visual person. I like seeing screenshots and videos, so yeah I'll take that to heart and do that for my own configuration as well. Sacha: When I post this, I'll probably… I figured out how to have the transcripts and then screenshots embedded into my transcript. I'll generate it automatically from the subtitle file. Our EmacsConf transcripts are going to get so fancy next year. But you can pull those screenshots and drop them into your config. It'll be great. Amin: Nice. Yeah, for sure. Sounds good.
Amin: And then here, I just enable some of these commands that are disabled by default. So yeah, it's useful, especially narrow-to-page, for example, or narrow-to-region. These are commands where Emacs disables them by default so that newcomers don't accidentally hit them and get very confused by what just happened. It doesn't disable them for good. It just basically prompts you for confirmation. Are you sure you want to run this command? I'm sure, at least about these commands. So I just enable them. And then something like, for example, overwrite-mode, which I never use and I don't want to accidentally enable. I just put it disabled so that if I do accidentally hit the keys, which might be, I don't know, something insert or whatever, then it will prompt me to make sure that I meant to do that. Sacha: That reminds me, I should probably turn that off for myself and then you get a whole new keyboard shortcut you can use too. Amin: Right, yeah. Let's see.
Amin: Yeah, I have just one line setting for package.el. In Emacs 31, we will be getting a package-review-policy which is very helpful. So if you do use package.el for installing packages from GNU ELPA, NonGNU ELPA, MELPA or whatever else, you can enable this, and then whenever you update your packages, you'll get a diff of what changed in this new revision of the package that you're downloading and you're about to enable. And you can presumably say yes or at least see what's going on, which I'd find helpful. Sacha: But you're not using packages, you mentioned, so you're just checking everything out and then you're just git pulling whenever you feel like it. Amin: Yeah, so right now I'm using git pulls and git submodules, very manual. I put this here because I think it's generally a very welcome change and awesome new feature that I want to spread the word about. So maybe someone who's looking at my config, they use package and that's perfectly fine. So this is just here to spread the word about it mainly, I guess. And if I start using package at some point myself in the future, then I will have this enabled. Let's see.
Amin: Very quickly, here I extend Info-directory-list. I like to, at least on some of my machines, use Emacs that I built from source directly in the source repository of Emacs. Just after doing make, I don't run make install, even though it's very easy to do that. You can install to a custom location by providing --prefix when you're ./configure-ing Emacs. Sometimes I just find it more convenient for me to not do that and just run make and then exit and reopen Emacs. And for that kind of a setup, I just extend the Info-directory-list to include the info subdirectory of the Emacs source repository so that the built-in Emacs info manuals will be available to me.
Amin: And then I use recentf for tracking recent revisited files. I bind it to C-c f r e for me to get a pop-up completion for visiting a recent file, it has completion. So if I hit TAB here, for example, we can see some of these files or directories that I visited recently. Sacha: I see. And then you're adding the directory to it. So what does that let you do? Because I'm assuming you're already in there in the directory. But how does that change your recentf? Amin: Right. So I need to think to remember this, but I think the point of this was that if I open a project in VC or in Dired, then I would like that directory to also get added to my recentf files list, because I think by default, recentf only includes files, not directories. Sacha: You're in it, you start up Magit or whatever, and then you move on to something else, but you want to be able to easily go back to it. Amin: Yeah, for example, I like to keep my recently visited directories in recentf as well. Because that's one of the main ways I jump between projects and stuff, even though there is literally a built-in Emacs project mode, which I still use. The only thing that I have here is… I don't want to add my home directory to the recently visited list, so the only thing that this function does is to skip that if I'm opening the home directory. That's about it.
Amin: And then here I configure mouse and scrolling behaviour. So I want Emacs to scroll very gently, one line at a time. I think the default is that when you reach the end of the page, it'll jump half a page down and then recenter. I don't remember default behaviour because I don't use it very much, but yeah, this basically makes it very predictable. For example, when I reach the edge of the page here and I press C-n, it'll only scroll one line at a time, instead of jumping and then doing something like this. Sacha: Oh yeah, mine does! Mine doesn't do that, so it does that jumping thing. I see what you mean here. Interesting. Amin: Yeah, so you can tweak that with scroll-conservatively and then scroll-preserve-screen-position, I believe.
Amin: Yeah, and then I use autorevert, which is pretty helpful. So this will have Emacs watch, for example, files that are open in your buffers. And if they change on disk, Emacs will automatically refresh the buffer so that you get the latest version. The cool thing is you can press undo in one of these files that's been autoreverted so that you get the revision that was there right before the change. So I've used that sometimes as well. Sacha: Yeah, and sometimes autofollow also is nice for log files and things like that. But yeah, autoreverting is great. Amin: Yeah, for sure.
Amin: repeat-mode is something that I've only recently started using, especially with my Emacs EXWM setup, using Emacs as my window manager. For example, if I hit C-x o, we see here in the echo area where it says repeat with o or capital O. So I can now only press o instead of saying C-x o, C-x o to do that multiple times. Keymaps that have support for this basically indicate that they want to be repeatable can declare that. And then once you invoke one of the keys in those keymaps, then you can repeat it with just that single character. And for example, for my setup, I have that with my EXWM workspace switching keys. So I can easily go to the next and previous workspaces, many of them at a time by just pressing p and n instead of doing the shortcut multiple times. Sacha: And actually, if you don't mind jumping ahead, the EXWM part of your config is fairly complex, and I think not a lot of people have a lot of experience seeing EXWM in action. And I don't know whether you're comfortable sharing you switching around to different workspaces, but if that is something that you can do, how are you doing all this awesomeness? I'm still too scared to use EXWM myself. Stability. But that's a me problem, not an EXWM problem.
Amin: Yeah, EXWM was pretty awesome. I used it back in 2018, '19 for a while, and then I kind of moved on to Sway and Wayland. But I don't know. It's something that I feel like once you try it, you want to keep going back to it. So recently, this past month or so, I decided to give it an earnest try and try to actually address any pain points that I've noticed. So it's much more usable for me now, and I'm sticking with it for now. I'm not a Wayland hater, but I'm just saying, at least for now, I'm using EXWM. And I'm happy to talk about it. Sacha: OK, what do you love about your setup for that one? Amin: EXWM? Sacha: Yeah, yeah. Like, you're doing a lot of rename buffers. Yeah, yeah, yeah. Amin: Right. Yeah, let me think. There's a couple of things. So, for the longest time, my Emacs EXWM configuration used super key as a prefix, which is the Windows [key] or the one with the logo, basically, to switch workspaces, launch applications and such. And at least the way that EXWM is right now, it doesn't… Like the way you have to add those global key bindings kind of slows down the EXWM startup. And I had many such key bindings.
Amin: So one thing that I did kind of recently is to define a prefix map here, like bandali-prefix-exwm-map. So I bind all of the keys and commands that I want here, and then this helps me really minimize what I'm telling EXWM, which is here. For example, this is how you set global keys with EXWM, and I just point it to my prefix map. C-c x and then any of those letters and functions that we saw. That's kind of annoying. I still use the super key here, but I have it s-x and s-,. On the left-hand side of my keyboard, x is right next to super, so I can hit it in one go with one motion almost as a single key with these two fingers. On the right side of my keyboard, I don't have a super key, but I have a control key that I remapped to super. On the right side, I do s-, with these two fingers. It's still very convenient for me to invoke those commands. And pairing this up with repeat-mode, as we can see just here, actually, then I can hit s-, and then p, n, or h, j, k, l many times to switch workspaces or shift focus to different windows and stuff without having to hit that kind of annoying s-x or s-, repeatedly. Yeah. Sacha: That sounds really cool. I should look into that. Sorry, quick aside.
Amin: Some of these things, like browsers, I still do them frequently enough, and I use different browser profiles. So I just define a new keymap so I can basically one-shot launch Chromium or Firefox in a specific browser [profile] or an incognito window and such. So yeah, I just do s-x b and then, for example, c to launch Chromium and all that stuff. So I found this pretty convenient.
Amin: Speaking of key bindings, before I get down this, let's see if I can find… C-c h. I think this is just before my EXWM setup. I'm pretty proud of this. I love this. It really goes to show how awesome Emacs is and extensible it is. Let's see. So as we know, these various help commands and describe commands are under C-h prefix. But some of them are not bound, for example, find-library or describe-face. Some of these I use pretty frequently. I was really having trouble coming up with descriptive-enough keybindings or short-enough keybindings for all of them. I put some of them here, for example, like C-c f l for find-library. But I can't do that for all of them. What I did was just do C-c h a or C-c h d. What this will do is basically, if I show that, It basically opens up M-x, fills in describe-, and then I can just type, for example, face, and that's it. So it basically opens up the minibuffer for me, pre-fills it with the string that I want, and I can type what is it that I'm looking for. And I found this to be better than trying to bind a million different keyboard things for describe this and that, apropos this and that, find this and that. So yeah and the way that we do that is to just use a minibuffer-with-setup-hook, and you just have a little lambda to insert the string that you give it, and then you invoke it. Sacha: Yeah, this is pretty cool. When I saw that in your config, I was like, I'm going to steal that. Pre-filling the minibuffer but still letting you do stuff with it, it's such a powerful thing, not just for completing the command itself, but even for when you're using the command, but you want to do something with the input before. You don't want to do it all the way, send it in and submit right away. You want to actually do something with it after you insert it. So great tip. Amin: Yeah. Thanks. Yeah, it's pretty useful. It's pretty nice. Yeah. And then back to the Emacs or EXWM stuff. So before I had, I used to yeah, sorry, go ahead. Sacha: Sorry. I forgot whether I was muted or unmuted. Amin: Okay, no worries.
Amin: For the longest time, I had 10 default EXWM workspaces on startup, and that can slow things down a little bit. So I found that okay, I don't really use all 10 workspaces always. So I set it to 5. So I get five workspaces initially. But I still bind keys here. Like if we go down. Let's see. Here. So here, I define those keys for all the way from, let's say, from 0 to 9 for all 10. And then if I try to switch to a workspace that doesn't exist, then EXWM will just go ahead and create it for me. Yeah, so I found that pretty cool. You can create workspaces on the fly. Yeah. Sacha: Yeah, and I saw that it moves your current window there, too. So that's just like, OK. Let's move it to workspace number two or whatever. Very cool. Amin: Yeah, yeah, yeah. I have keys or convenience keys for moving some window to some workspace. Yeah, it's nice. Let's see. Let's see. Yeah. So these are just made key bindings. I use hjkl here for switching windows.
Amin: I also have a ZSA Voyager split ergonomic keyboard. I can basically customize it infinitely. For example, I don't really have a super key on the first layer. What I have is a key that will do the s-x thingy, basically, my prefix. So that's the last missing piece is that if I'm at home and if I have this keyboard with me, then I just hit one key and then that's it. I'm in my prefix. But even if not, on the laptop, the s-x or the super comma are still easy enough for me to hit it with one hand. Sacha: Now I'm jealous and I definitely want to assign my prefixes to their own keys. Very tempting. I've started using the numpad because my laptop has one. I only use the numpad rarely, but we all need more keys. Amin: Yeah, ergonomic keyboards are pretty nice, especially these ones. For example, the ZSA ones where you can put QMK on it, the QMK firmware. You can define keys in a C file. I can actually show that. Let's see… QMK Firmware, Keyboards, ZSA, Voyager, Bandali, and then keymap.c. Sacha: Is this in your repository somewhere? Amin: Right. It's in a different repository, but it's still on https://git.kelar.org next to my configs repository. You can find this as well, but if I go smaller… Yeah, you can define keys here and have different layers, like the base layer. And then you can define a key to switch between different layers and put some of the keys there anyway. So yeah, it's a whole rabbit hole in and of itself. Prot also uses a split ergonomic keyboard. It really does help if you're typing for long periods of time. I actually had these for a while, and I wasn't using them too much, but I started slowly getting some pain in my wrists and here. So I was like, okay, I have the keyboard, might as well put it to good use, and I've started using it.
Sacha: Okay, so most of your keyboard shortcuts come off that kind of s-x or C-c something, and then you have a long prefix sequence, and you just remember everything or you use your… pre-fill some of it and then fill in the rest of the command. Amin: Pretty much all my window management related keys are on this s-x prefix that I'm showing here. And then I have a few other ones which I think I showed earlier. Is it this one? Anyway, I bind a few general keys outside of the s-x thing, like C-c e i. For example, I have C-c e e for eval-last-sexp. I do that a lot, so it's easy to hit that. Making frames or deleting frames. Sacha: I love how Emacs uptime is something you use frequently enough that you have a keyboard shortcut for it. Amin: Yeah, of course. I mean, I'm sometimes curious to see how long has my Emacs session been running. To continue with the EXWM stuff, let's see. This is just some keybindings I define here. It's all Emacs Lisp, right? It's amazing. You can mapc over whatever sequence and create keybindings like that. Only with Emacs we can do things like that. I just love it. Let's see.
Amin: I still keep these three other keys for raising and lowering the volume and toggling mute off of that prefix and just directly on my keyboard, hitting it directly in the exwm-input-global-keys because I do that very, very frequently. But I also have scripts that I can invoke. I should do keycast. So yeah, I can invoke the prefix with semicolon. I can set my volume here, adjust it here, type in what volume I want, or with the single quote, I can enter a value for the screen brightness. I like these things to be exact depending on the lighting in the room. I have preferred brightness values of 50 or 12 or 10 that I manually adjust. I guess it's a poor man's version of having something with a light sensor that can pick up and adjust automatically. I do it manually. Yeah. Sorry, you just muted yourself again. Sacha: You're just probably this close to writing the Emacs Lisp that takes your webcam image and then adjusts your light. But I think Prot was also saying he likes to do the lighting changes manually as well because warmer colors versus cooler colors and all of that stuff. Anyway, so you have all these buttons that EXWM listens to and it can launch various things for. That's a lot of things. Amin: Yeah, those are pretty cool.
Amin: EXWM has this lovely feature called input simulation keys where You can basically use it to bring Emacs key bindings to other applications like Firefox or whatever. And yeah, it's mind blowing when you try it for the first time. for example, I bind C-b to just hit the left arrow on the keyboard. And it does that. So I can define all of these commands that I'm using or used to using in Emacs. So I can get them in Firefox or other applications as well. Realistically, it's mostly Firefox. It's the only other program that I spend any reasonable amount of time outside of Emacs. Sacha: Let me point out this very important one that you have there. Under selection/cut/copy/paste, I see a C-w input simulation key. So this is for all the people who have accidentally closed their browser tab while trying to copy text. This is how you solve that problem. Use EXWM and use EXWM input simulation keys and you don't have to accidentally close your browser tabs again. @blaiseutube asks, hey, what about time since last save? Or do you have some kind of autosave magic? you know, in reference to the uptime thing, right? You have this thing that shows you… Amin: I don't think I have anything for autosave, but I have this habit of… I save everything pretty regularly. Yeah, so I've never really needed that feature, but I'm sure Emacs has something where you can, at the very least, just very dumb, simple implementation of has it been idle for one minute, then just do a save buffer. You can roll your own. But I don't have anything. Sacha: All right. I'm getting really tempted now to try out EXWM, even if it's just for those global keyboard remapping things.
Sacha: How is it for windows that you've got to have floating? I feel like it's very good at handling tiling things, but how is it for sometimes the apps kind of really want the floating window? Amin: Right, yeah, so you can toggle any window to be floating or not, and you can also - actually, we're just looking at it here. EXWM manage configurations, to match on the instance name or the class name of a window that you can get from xprop, to automatically make that tiling. For example, if I do my prefix and then capital T, it launches a floating terminal for me here. And if I go back to where I set it up, I just launch Xterm with the -name argument. This is where it can set the instance. And I just put any string you can want, like floating, for example. And then here in my configuration, I just check that if the instance name is floating, then I'll go ahead and float the window. Simple as that. Sacha: All right. This is starting to look exceedingly tempting. Lol, I save everything regularly, so he's one of those people who compulsively hit C-x C-s. Amin: Yeah, I do that a lot. I don't know. It's just me. But, yeah. Yeah. And then, I don't know. EXWM is awesome.
Amin: You can also put local simulation keys, application-specific simulation keys, depending on, the application, terminals, for example, or, Zathura. This is a PDF viewer. To have application-specific custom key bindings, how cool is that? For example, if I'm in Xterm or something like the Mate terminal, hitting C-c C-c twice basically, it'll just send the C-c key to the terminal. Because one thing with EXWM is that you can set it to capture a couple of Emacs prefixes, like C-x or C-c. So the application by default doesn't see it because Emacs captures it. But this is one of those mechanisms by which you can send a key through. Let's see.
Amin: So this thingy here, I enable EXWM and I add this rename hook and all it does is basically to add the window titles to the buffer that I can see on the mode line. But as long as it's within a certain reasonable length, like for example, I have 25 characters. If it's longer than that, it will just put dot dot dot (...). So yeah, that's all the purpose of that. Let's see, for example, if I launch Xterm, it appears there. The perfect example is actually here on the right-hand side. On the mode line, we see Firefox ESR Emacs Chat. It's a bit long, so it just puts the dot dot dot there. So that's all that does. Sacha: Yeah, now being able to use Emacs to manage the tiling of these things instead of my having to fiddle with alt-dragging things to snap nicely into buffers. Yes, very cool stuff. EXWM. Gotta try it. Amin: Yeah, for sure. Yeah, let's see.
Amin: Here I launch Dunst if the executable is installed for getting notifications in EXWM. I think there's at least one or two Emacs specific packages that implement a simple notification daemon or backend so that Emacs itself can handle that. But I found Dunst good enough for my use cases coming from i3, Sway, like tiling window manager background. I just reuse that. So yeah, I just start a process, keep a handle of it in this bandali--dunst-process variable here. And this thing I discovered recently, it's cool. using set-process-query-on-exit-flag, you can basically have Emacs not ask you if you want to exit Emacs if that process is still running. It'll just kill it without confirming with you. So just a little convenience. Sacha: That is also cool. Just a heads up, I have about 15 minutes before the kiddo runs out because she'll be done with school then. Even just the EXWM part and other things that you've shown us in the config have been super awesome. But are there other things in the next 15 minutes that you would love to show people so that they can see how it works in practice?
Amin: One thing I'll just mention, EXWM, one more thing, and then I'll go check. I think this is kind of recent: EXWM xsettings, and this allows you to dynamically at runtime change some of these things that you would normally set in an X resources file, like fonts. These kinds of settings were especially commonplace back when Wayland wasn't a thing or wasn't very popular. You would set some of these font settings there. With EXWM xsettings, you can do this dynamically, and what's awesome about that is it also lets you hook into, for example, if your screen configuration changes, if you plug in a monitor or unplug it, then you can run whatever xrandr command to set it up and also adjust those settings. The main thing I use it for is to change the DPI setting. The thing with X11 or Xorg is, unfortunately, there's no per-monitor DPI. There's one global DPI. But I found that on my high-DPI laptop screen, if I set the resolution to 1920x1080 instead of the full resolution, then the default DPI of 96 works just fine with my external monitor as well. All this little hook does, by calling into this function, is: if I'm plugging in my external monitor, lower the resolution and lower the DPI, and if I unplug it, go back to the high thing. I just love this. Sacha: That's great. We're definitely not going to demonstrate that because plugging in and unplugging monitors is not a good thing for screen sharing, but that sounds really cool. When things change, you can actually get your system to adapt to the changes for you. Amin: Yeah, it's lovely. Let's see. There's so much more to talk about.
Amin: I have written some things about the prompt for this meeting. Yeah, so I talked about that stuff briefly. Minibuffer setup. Things that I love about my setup is that it's kind of portable, simple. People can easily copy things from it if they want. It's kind of self-contained. And that was kind of a big thing a while back when I wanted to use my configurations on a couple of work machines. And these don't have direct outbound internet access. So I couldn't do things like installing packages with ELPA because that's done over HTTP. So yeah, I use submodules now. I recently began documenting my setup, very much inspired by Prot and Sacha and others.
Amin: This is my configurations repository. If you go here to treeview .emacs.d, this is the org file. I also export all of those individual components into this lisp subdirectory. All that stuff is here. The QMK thingy that was mentioned.
Amin: Oh, I wanted to mention FFS. Okay, I'll do that as well. Yeah, what's up with that? Sacha: I was trying to find information. It was like, there's no package. It's not what is this thing? Amin: It's FormFeed Slides and it's going to soon be a package. I was actually talking to Prot about it and I'm hoping to submit it for inclusion in GNU ELPA within, I don't know, the next couple of weeks. It's basically very similar to Prot's Logos package. Turns out we both had the same kind of idea at the exact same time in 2022, and we both used it for our LibrePlanet 2022 presentations. Of course, Prot being the diligent person that he is, he polished his work, documented it, put it on GNU ELPA. I still haven't gotten around to doing it yet, but better late than never. Yeah, let's see. I can maybe show a quick demonstration of that. So let's see. Let's see. Anyway, so if I go to my website sources and net-beyond-web. So I had the LibrePlanet talk a couple years ago. So what FFS is basically, it looks for a particular character in this case, or the default case, it's the page-delimiter, ^L, which you can insert by hitting C-q C-l. It basically then designates each of these areas as one slide. So, very, very simple slideshow that you don't even have to use Org or outline or any other major or minor mode. If I launch ffs, by default, it's in a mode where it binds a couple of convenience keys, like p and n, to go into the next and previous slide. You can hit e to edit a slide, similar to Org source, and then make your changes and all of that. And then you can start a presentation by hitting s.
Amin: It has hooks for, for example, bumping up the font size or whatever, hiding the mode line. I can toggle the mode line by hitting m here. Let's see. I can also toggle the cursor with c, to make the cursor visible or not. So, yeah. And then I'm just hitting p and n. Sacha: Very simple, very minimalist. You have a file, you've got page markers, and that's all you got. Amin: Yeah, pretty much. And then…
Amin: So you can designate a file as being the speaker notes where it has the same structure separators with ^L. But you can type your notes over here, whatever. And you can basically open these in two different windows or two different frames on separate displays. And then in whichever one of those you advance the slides, like p n n, it also does the other one. Sacha: That's brilliant. I was looking for a way to do that so I can pretend to know what I'm talking about when I have something on screen, but I can just read my notes or even just remember what points I wanted to make. So this is great. You have speaker notes. You've got the main screen. They can be in two different frames. You can have your frame that you're sharing and your frame that you're not sharing that has all of your cheat sheets. Excellent. And on that note, in about one minute, the kid is going to come running out and want to have snack and all that stuff. Thank you so much for walking through parts of your config. There is more. And so everyone who wants to find out more can go check out your setup. I have a great many things that I want to try out, starting from EXWM to little things like figuring out a boom mic setup because apparently your audio setup is making me very jealous. Yes, thank you for doing this. I'm going to post the transcript and the chapters. I have a chapter every minute. It's going to be a long time. But it was good. Lots of cool stuff. Thank you again. Amin: Sounds great. And yeah, you're very welcome. And thank you so much for having me as well, Sacha. I'm very delighted to be here, especially, I think, just by chance. I think I'm the first person who you're doing this with after the long hiatus. So that's an extra honor for me. But yeah, it's been fun. I could go on for hours. I'm sure we both could. This has been fun. Sacha: If we wanted to go on for hours, Prot has more flexible scheduling, so he can chat with people for two hours and stuff, and you already have conversations going on with him. But I unfortunately have a small mammal who's 10 years old and loves me very much, and likes to not let me concentrate for very long. But thank you everyone for joining. Thank you for the chat. And thank you also, stream, for all the interesting questions. I will send you all the information and update the post. And we'll see you all on Thursday. I've got another chat. All of a sudden, all these Emacs chats are going to happen. Thanks. Oh, and you said you're happy to be on the hook for doing another EmacsConf this year, right? Amin: Yes. You can hold me to that. There will be another EmacsConf this year and I will be active in it. Sacha: Alright then, I'm going to end that broadcast. Thanks everyone, bye! Amin: Thank you, bye bye!Find more Emacs Chats or join the fun: https://sachachua.com/emacs-chat
You can comment on Mastodon or e-mail me at sacha@sachachua.com.
-1:-- Emacs Chat 21: Amin Bandali (Post Sacha Chua)--L0--C0--2026-05-04T18:28:11.000Z
David Dimagid wrote this post for Emacs Carnival May 2026: "May I recommend…". Here it is!
Someone recently said on emacs-devel that they'd like to talk about recommending ELPA packages. Someone else said we should first ask what "recommending" actually means. RMS opened a thread asking that very question. It's still open, and you can follow it there (ELPA: to curate or not to curate).
I think we could apply Rich Hickey's technique here and start by looking up the definition of "recommend" in the dictionary. I invite everyone to do so with whatever dictionary you have at hand and to trust your definitions.
Now, we could evaluate ELPA packages for recommendation based on whether they complement or improve functionality already present in the core. For example, diff-hl by Dmitry Gutov. Its description says:
diff-hl-mode highlights uncommitted changes on the side of the window, allows you to jump between and revert them selectively. In buffers controlled by Git, you can stage and unstage the changes.
That last feature —staging partial hunks— is missing from VC, and diff-hl adds it seamlessly. We could say diff-hl complements the core.
Then there are major mode packages, like csv-mode, markdown-mode, cobol-mode, and so on. They add functionality that doesn't exist in the core. They have no direct equivalent. We could call them standalone packages.
Now consider another excellent package, like diff-hl, that depends only on the core: expreg, by Yuan Fu, the region expansion package. With a single key, it expands the region based on context. The core already offers this through sexp movement commands, but not with a single keybinding — you need several. Some will prefer the native core way; others will prefer the package. We could say expreg improves or, depending on how you look at it, duplicates the core's functionality.
So, in my opinion, package recommendations should be structured around their relationship with the Emacs core. I believe the best-regarded ELPA packages should be those that encourage users to use what the core already offers, first and foremost, and then try those packages because they extend a feature the core lacks or complement it. This would also help more people discover lesser-known core features, increase bug reports, and, over time, bring more contributors to Emacs. That way, the Emacs community could have a package repository it can trust for as long as Emacs exists. Perhaps the person who wrote Elfeed would have known about Newsticker and would have contributed to that package instead. Perhaps if we recommended what Emacs already offers, the Elisp we write would be Elisp of and for Emacs.
If you e-mail me your comments, I can forward them to David!
You can e-mail me at sacha@sachachua.com.
-1:-- From David Dimagid: What we talk about when we talk about recommending Emacs packages (Post Sacha Chua)--L0--C0--2026-05-04T17:06:00.000Z
It's May and I like puns, so I'm going to suggest "May I recommend…" as our Emacs Carnival theme this month, building on lively conversations about people's favourite packages on lobste.rs, Reddit, and Hacker News. Let's go beyond packages and talk workflows, tips, practices, perspectives… whatever you'd recommend!
It was pretty nice having a wiki page that people could edit without needing to wait for me, so if you write about this topic, feel free to and add your link. If you run into problems doing that, please e-mail me and I can add the link for you.
People have already started sharing their recommendations:
I'll also do a round-up post at the end of the month so that it shows up in people's RSS feeds.
Looking forward to seeing what y'all recommend!
You can e-mail me at sacha@sachachua.com.
-1:-- Emacs Carnival May 2026: "May I recommend..." (Post Sacha Chua)--L0--C0--2026-05-04T16:50:38.000Z
Thanks to everyone who shared their thoughts on the April 2026 Emacs Carnival theme of Newbies and Starter Kits. Check out that post to see all the entries people have shared so far. I enjoyed chatting with Prot about the topic, and he shared some defaults that even experienced users have been trying out. The carnival theme for May 2026 is "May I recommend…". Looking forward to reading your posts!
Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!
You can e-mail me at sacha@sachachua.com.
-1:-- 2026-05-04 Emacs news (Post Sacha Chua)--L0--C0--2026-05-04T13:08:22.000Z
Tools
Update: There are some legal issues associated with the Notepad++ product I talk about here. Trademark Violation: Fake Notepad++ for Mac | Notepad++. The original author says that the macOS version is an unauthorized clone. I am not going to cover this product. The macOS version has been renamed to Nextpad. Sounds shady as fuck.
That is all I have for today.
macosxguru at the gmail thingie.
Note: Thanks to Skylar Kang for the picture.
-1:-- Links of Note 2026-05-04 (Post Bicycle for Your Mind)--L0--C0--2026-05-04T07:00:00.000Z
Recently I realised that it'd be really nice if jumping to errors would store the previous location in the Evil jump list. These definitions do just that
(evil-define-motion mes/evil-goto-next-error (count)
:jump t
(unless (bound-and-true-p flymake-mode) (signal 'search-failed nil))
(flymake-goto-next-error count))
(evil-define-motion mes/evil-goto-prev-error (count)
:jump t
(unless (bound-and-true-p flymake-mode) (signal 'search-failed nil))
(flymake-goto-prev-error count))
and for now I've bound them to C-j and C-k (because that's what
evil-collection does)
(general-def flymake-mode-map
:states 'normal
"C-j" 'mes/evil-goto-next-error
"C-k" 'mes/evil-goto-prev-error)
This makes it easier to make a change, fix the errors caused by the change and then return to where I was.
-1:-- Jumping to errors in Evil (Post Magnus)--L0--C0--2026-05-04T06:16:00.000Z
Raw link: https://www.youtube.com/watch?v=UqfZZRl_eNw
On the 14th of May we will meet with Sacha and Philip to talk about
the upcoming Emacs 31 and other developments heading into Emacs 32.
Philip is a contributor to core Emacs and the main driver behind the
new newcomers-presets theme, among others. We will cover themes
related to the newcomer experience.
I am looking forward to it!
-1:-- Emacs live with Sacha Chua and Philip Kaludercic on 2026-05-14 17:30 Europe/Athens (Post Protesilaos)--L0--C0--2026-05-04T00:00:00.000Z
This is an excerpt from an exchange that I am reproducing with permission from my correspondent. I am not sharing their contact details.
As I understand, you’re using the regular keybindings in emacs. I’m trying to transition away from the vim way of editing text and was wondering if you’re using a more ergonomic approach for your text editing needs. I remember you were using some ergo split keyboards for that?
Indeed, I have a split mechanical keyboard (a gift from a person who identifies as “Andreas”). It is the Iris revision 8 by Keebio. I also have a regular full-sized keyboard (a gift from Arialdo Martini), which is the Keychron K5.
[ Keyboard-related articles of mine: https://protesilaos.com/keeb/. ]
The split keyboard is nice, though it is not a magical solution. Even
with a regular form factor keyboard, I can work with the standard
Emacs keys without any problem. What helps me the most is to configure
one-shot modifiers. This means that I can tap (press once, then
release) a modifier key, then tap a regular key to register it as a
modifier+key event. For example, C-x is this: tap Ctrl, then tap
x. Shift tapping is especially nice for prose, by the way, and
eases the pressure on the otherwise weak pinky. The time window for
registering a one-shot modifier is configurable.
The other useful tweak for a regular keyboard is to rearrange the layout of the modifiers. I prefer this order:
Super Alt Ctrl Space Ctrl Alt Super
[ If there is a Menu key, move it to the right corner and consider
assigning it to the Compose key if you are on Linux. ]
Now Ctrl is under the thumb, which is especially nice for Emacs.
Alt and Super can be swapped, if you use Super more heavily
(e.g. with a tiling window manager). But the important part is to keep
Ctrl close to the Space key.
Whatever you do though, remember to use both sets of modifiers. For
example, C-x involves the right hand for Ctrl and the left hand
for x. C-p needs the left hand for Ctrl and the right hand for
p. This way you distribute the burden so no one side is overworked
(and, of course, you take regular breaks from typing altogether).
Lots of Emacs users will rebind Caps Lock to Ctrl. While this is
fine in its own right, it tends to embed bad habits, such as with
using only the left hand to register the very common C-a, C-e,
C-d, C-f, C-s, C-r, C-w, C-g, C-z, C-x, C-c, C-v.
Exclusive left-handed use involves awkward twisting of the muscles
which will probably hurt you over time. Plus, the left pinky is forced
to press and hold a key while being stretched—looks bad. Again,
distribute the load.
The final part that makes everything easier is to configure a “layer”
key. When you press and hold this key, other keys register a different
input than normal. For example, Caps Lock can be the layer key,
which then makes h, j, k, l act as arrow keys. Same idea for
mapping Home, PgDn, PgUp, End someplace that makes sense (mine
are on y, u, i, o). I prefer to have Space as that layer
key: it only is a layer when I press and hold it, otherwise it
performs the ordinary function of the Space key.
It is okay to press and hold Space because you do it with your
relatively strong thumbs. Better have it this way than pressing and
holding with the pinkies.
All this can be achieved with software such as kanata. A custom
keyboard with QMK firmware can get the same configuration embedded
directly in the keyboard (so it works without any special program
running on the computer). Kanata will be the cheaper solution and is
probably better overall if you consider that it can apply to a
laptop’s keyboard.
A mistake in all this is to think that an expensive keyboard is inherently more ergonomic. If you keep curling, overextending, or anyhow stressing your muscles the injuries will occur regardless. A new keyboard can help if you use it as an opportunity to retrain your muscle memory.
Custom keyboards have potential advantages in terms of comfort because you can change the switches and keycaps that they use. For example, you can pick a lighter or heavier switch to match your typing technique. Then you can combine it with a shallower or steeper keycap profile to get the most out of that setup. There is no right or wrong here. It is a matter of optimising on top of the strong fundamentals that I outlined above.
As for evil-mode in Emacs, I think it is a good solution overall.
You do, however, need to install the evil-collection package and
probably also configure lots of other key bindings to get exactly what
you want. There are other packages that give you modal editing, though
I have only ever used evil-mode in earnest: it is fine.
That granted, I find that I do not like modal editing in general. It is especially inconvenient for me when I write at length (which I do a lot) because I tend to produce a wall of text in one go. Having a modal interface gives me no advantage in this common scenario. I also doubt it ever gave me the edge while programming. The bottleneck is how quickly and clearly I can think, not how fast I can edit lines of text (though, yes, Vim’s paradigm is powerful).
Consider then the overall comfort of your setup. Both in terms of the ergonomics of hardware but also how much effort it takes to maintain your Emacs configuration. The standard approach to key bindings gives you something that “just works” with practically every package you install. It may feel awkward in the beginning if you are coming from the Vim keys, but will be the most robust solution long-term from the perspective of maintainability.
-1:-- Re: Emacs and keyboard ergonomics (Post Protesilaos)--L0--C0--2026-05-04T00:00:00.000Z
I love teaching. I do not love grading, though. I’ve always thought it would be an ideal job if there were no requirement for grades, and if there were no grades, then there would be no need for exams. This, of course, isn’t true. In an ideal world, each student would be motivated to acquire knowledge, wisdom, and understanding, not a mere grade. Good students would still want some kind of assessment, so that they could know if they had reached their desired level of understanding. It seems that I can’t escape the necessity of testing.
I prefer to give essay exams in my upper-level courses, but the larger sizes of introductory courses make that impractical. Multiple-choice questions are easy to grade, but good ones are difficult to write. It would be nice to have a question bank from which I could easily choose questions to include on an exam, all stored in an easy-to-write format that could be used to produce both online and print versions of a test. This sounds like a perfect task for Emacs and Org mode.
Two years ago, I posted my method of writing quizzes in Org mode for import into Canvas, the LMS our university uses. I decided to use this format for multiple choice:
1. Multiple choice question (single correct answer)
a) Wrong
b) Wrong
c) Right*
d) Wrong
The multiple choice question bank will then consist of a collection of enumerated Org mode lists, something like this:
1. Why is global skepticism especially difficult to rationally respond to?
a) It requires accepting the existence of the external world before any argument can begin.
b) It is logically self-contradictory and therefore cannot be engaged directly.
c) There are no premises the skeptic would grant that could be used against the position.*
d) It collapses into local skepticism when pressed by careful argument.
2. What is Mackie's central claim in "Evil and Omnipotence"?
a) The existence of evil makes theism unlikely but not impossible
b) Religious beliefs about God and evil are positively irrational, not merely unsupported*
c) Omnipotence is logically incompatible with omniscience
d) The free will defense successfully resolves the logical problem of evil
3. Why is proving the nonexistence of the evil demon insufficient to establish knowledge of the external world?
a) The evil demon could simply be replaced by an equally deceptive force.
b) Disproving active deception still leaves open the possibilities of dreaming and naturally broken senses.*
c) The evil demon argument applies only to sensory knowledge, not to rational knowledge.
d) Proving a negative is logically impossible within Descartes' method of doubt.
I decided to have a frame split into two windows, the question bank in one and the scratch buffer in the other. I’d like to select questions from the bank and have them copied to the scratch buffer. Here’s the function that I wrote with the help of Claude:
(defun rlr/copy-mcq-to-scratch ()
"Copy the multiple choice question at point to the *scratch* buffer."
(interactive)
(save-excursion
(let* ((question-start
(progn
(end-of-line)
(if (re-search-backward "^[0-9]+\\." nil t)
(point)
(error "No question found at point"))))
(question-end
(progn
(goto-char question-start)
(forward-line 1)
(if (re-search-forward "^[0-9]+\\." nil t)
(match-beginning 0)
(point-max))))
(text (buffer-substring-no-properties question-start question-end)))
(with-current-buffer (get-buffer-create "*scratch*")
(goto-char (point-max))
(insert text)))))
If the point is anywhere in a question or one of its answer options, calling rlr/copy-mcq-to-scratch appends the question to the scratch buffer. I could use a keybinding for this, but I’m not sure I write exams often enough to waste a good keybinding. It’s easy enough to create a quick keyboard macro to select the first question and then press F4 for other questions.
If I want to create a Canvas Quiz, then it’s ready to be converted into a QTI file using the method I wrote about earlier. For print exams, I use the LaTeX examdesign class1, which requires this format for multiple choice:
\begin{question}
Multiple choice question
\choice {Wrong}
\choice {Wrong}
\choice[!] {Right}
\choice {Wrong}
\end{question}
To convert the questions, I select the entire scratch buffer and call this function. It formats the questions properly and inserts a blank line between each of them.
(defun rlr/org-mc-to-latex-questions (beg end)
"Convert org-mode multiple choice questions in region to LaTeX format. Questions are numbered lines followed by lettered choices (a-z). Correct answers are marked with * after the choice text."
(interactive "r")
(let* ((text (buffer-substring-no-properties beg end))
(lines (split-string text "\n"))
(result '())
(in-question nil))
(dolist (line lines)
(cond
;; Numbered question line: "2. Question text"
((string-match "^[[:space:]]*[0-9]+\\.[[:space:]]+\\(.+\\)$" line)
(when in-question
(push " \\end{question}" result)
(push "" result))
(push " \\begin{question}" result)
(push (format " %s" (match-string 1 line)) result)
(setq in-question t))
;; Choice line: " a) Choice text" or " a) Choice text*"
((string-match "^[[:space:]]*[a-z])[[:space:]]+\\(.+?\\)\\(*\\)?[[:space:]]*$" line)
(let* ((text (match-string 1 line))
(correct (match-string 2 line))
(tag (if correct "\\choice[!]" "\\choice")))
(push (format " %s {%s}" tag text) result)))))
(when in-question
(push " \\end{question}" result))
(let ((output (mapconcat #'identity (nreverse result) "\n")))
(goto-char beg)
(delete-region beg end)
(insert output))))
The next tasks are functions for easily removing and reordering questions. For that, I should probably wait until finals are over, though.
Now, unfortunately, it’s back to grading.
EDIT (May 4, 2026):
I must not be able to think when it’s later in the day. Reordering the questions is easy. Since they’re just Org lists, M-up and M-down moves an item and its child lists up and down. Deleting questions is not quite as easy, but it still wasn’t difficult:
(defun rlr/delete-mcq-at-point ()
"Delete the multiple choice question at point, including all its choices."
(interactive)
(save-excursion
(beginning-of-line)
;; If on a choice line, move up to the question line first
(unless (looking-at "[[:space:]]*[0-9]+\\.")
(re-search-backward "^[[:space:]]*[0-9]+\\." nil t))
(let ((start (line-beginning-position))
(end (progn
(forward-line 1)
(if (re-search-forward "^[[:space:]]*[0-9]+\\." nil t)
(match-beginning 0)
(point-max)))))
(kill-region start end)))
(org-list-repair))
I used kill-region to save the question to the kill ring, followed by org-list-repair to renumber the list. Killing instead of deleting is useful in cases where one might want to move the question farther than would be convenient with a simple reordering.
There are several LaTeX packages for producing exams. I’ve found that examdesign has the best combination of features for my needs. It can be used to easily print different versions of an exam to discourage cheating with an answer key for each version. It can handle five different types of questions, and has a block environment for questions that need to be placed in a group with instructions for the group. The questions in the block stay together even if the rest of the exam is randomly shuffled. If you need printed exams, I highly recommend it.
-1:-- Managing Multiple-Choice Questions With Org Mode (Post Randy Ridenour)--L0--C0--2026-05-03T19:31:00.000Z
Protesilaos (Prot) has a new, interesting package available: buffer-to-pdf. It does just what its name suggests. It takes the current Emacs buffer and exports it to PDF preserving things like theme, and font characteristics.
It’s easy to use. You simply call buffer-to-pdf and you get a PDF of your current buffer. You can specify the orientation—portrait, landscape, or current window—for the output but that’s the only choice you have to make.
The most complicated thing about the package is its pagination. If you have an Org file, each headline is a new page. The idea is to turn it the org file into a set of slides. Again, this all happens automatically.
For a simple text file, buffer-to-pdf paginates at screen borders. That seems like a natural thing to do and allows a certain amount of control over where the page breaks occur by adjusting the window size.
Finally, you can specify explicit page breaks by inserting a form feed (^L). Prot’s video doesn’t make clear how the form feeds interact with the window boundary heuristic but, as Prot says, the best way of thinking about buffer-to-pdf is as a screen capture that produces a PDF. It’s a perfect way to share an Emacs buffer with someone else.
If you’re interested in this package, take a look at Prot’s video at the above link. You can check out the package’s Git repository here.
-1:-- Prot’s New Org-to-buffer Package (Post Irreal)--L0--C0--2026-05-03T14:42:33.000Z
I’ve been looking for (or rather, chasing!) the right note-taking workflow in Emacs for over a decade now. Back in 2013, I wrote about setting up Deft mode with Org-Mode and it was my attempt at bringing that Notational Velocity-style simplicity into Emacs, and I used it for a long time. I genuinely liked Deft, but it’s no longer actively maintained, and the same goes for Zetteldeft which was built on top of it as an alternative successor. Since then, I’ve tried the other major players – org-roam, Denote, and a handful of others. They’re all extremely impressive and comprehensive packages that do more than I could ever ask for, but none of them ever quite stuck for me. I’d set one up and more or less force myself to use it for a few weeks, and then quietly drift back to a loose pile of org files in a directory. And on my darker days, Apple Notes.
The problem was never these packages – it was more me and my typical, simple workflow that I’ve grown so used to over the years. Learning a new tool carried implicit friction points that compounded over time. Org-roam is powerful, but it is opinionated about note creation - every note needs a unique org-id property and lots of frontmatter that I often had to lookup from previous notes to even remember what to include. The whole system, when used correctly, is backed by a SQLite database via emacsql that indexes your nodes and links. That’s a lot of overhead in my candid opinion. If the database gets out of sync or emacsql has compilation issues, you’re debugging infrastructure instead of taking notes. This was another pain point of mine since I often work on multiple machines (work, personal).
On the other hand, simple tooling like ripgrep is plenty fast for searching files, and removing the database removes an entire category of things that can break – especially for newer users who are already navigating the Emacs and org-mode learning curve. Denote is a great package that uses a lot of good, well-thought-out ideas and tooling, but I found it had a higher barrier to entry and is a bit opinionated in favor of note-taking workflows that I just never adopted or used over the years. This isn’t meant to be a knock on org-roam or Denote - it’s all my preference – but I wanted something that fit how I actually work without getting in my way - and something that I knew I would use long term. The note-taking workflow I wanted is something that felt closer to just opening a folder of org files, but with the modern conveniences that tools like Obsidian offers.
So I wrote grove.el.
Grove is an Obsidian-like note-taking mode for Emacs. One keybinding opens a full workspace with a file tree sidebar and your org notes. No external Emacs dependencies, no databases – just org files, a directory, and ripgrep.
The feature set covers the things I actually want and use daily:
[[note title]] syntax, click to follow, creates the note if it doesn’t exist#hashtag and org :tag: syntaxThe honest answer is that I wanted a note-taking system that I’d actually use. Every previous attempt had some piece that didn’t fit my workflow, and over time that friction compounded until I stopped using it entirely.
What I kept coming back to was how simple tools like Obsidian feels. Yes, I know - it’s basically org-mode but with a pretty GUI! But you open a vault, you see your files, you write notes with wikilinks, and the tool stays out of your way. The graph view is a nice addition - nothing new, but something that I knew I wanted to try and recreate in a less complex fashion. The sidebar is useful visually - often times it is very beneficial to focus on a single, solo buffer, but when it comes to note taking, visually seeing the other notes can add extra context without overloading you - enabling you to add categorization and linking with minimal overhead. My intention was to create something that doesn’t prescribe a methodology - you can do Zettelkasten, GTD, or just free-form notes, and Grove doesn’t care. I wanted that same feeling and purpose that I get when I’m in Emacs, where I already spend a good portion of my day.
Grove is intentionally less opinionated than many of the existing note-taking packages in the Emacs ecosystem. There’s no enforced file naming scheme, no required metadata format, no database to maintain. Your notes are org files in a directory. If you stop using Grove tomorrow, your notes are still just org files. I think that matters most in the end - when you stop using a tool, the underlying bits don’t change, including yourself.
At the same time, I wanted it to be batteries included. One of the barriers I’ve seen with Emacs note-taking setups is that getting everything working – the sidebar (especially this!), the linking, the search, the capture workflow - it all requires stitching together multiple packages and a fair amount of configuration. Grove bundles all of that into a single package with sensible defaults. The goal is that a new user can install it, point it at a directory, and have a complete note-taking workspace immediately. Whether you’re new to Emacs or have been using it for many, many years, the setup cost is minimal and investment low.
Grove requires Emacs 29.1+ and ripgrep. Graphviz is optional for the graph view, and Consult is optional for enhanced search.
Grove is currently in review for MELPA. Once it is approved and merged, you’ll be able to install it with:
M-x package-install RET grove RET
Or with use-package:
(use-package grove
:ensure t
:custom
(grove-directory "~/notes"))
In the meantime, you can install it manually by cloning the repo:
git clone https://github.com/jonathanchu/grove.git
(use-package grove
:load-path "path/to/grove"
:custom
(grove-directory "~/notes"))
The entry point is C-c v v to open the Grove workspace, or M-x grove-open. From there, the full keybinding set lives under the C-c v prefix:
grove
├── C-c v v Open workspace
├── C-c v q Close workspace
├── C-c v c Quick capture
├── C-c v f Find note
├── C-c v s Search notes
├── C-c v d Daily note (today)
├── C-c v y Daily note (yesterday)
├── C-c v t Daily note (tomorrow)
├── C-c v b Show backlinks
├── C-c v # Search by tag
├── C-c v i Inbox review
├── C-c v l Insert wikilink
└── C-c v g Graph view
The file tree sidebar tracks your current file, and you can expand/collapse directories with TAB. Quick capture (C-c v c) opens a buffer where you type your note - the first line becomes the filename and it’s saved directly to your inbox for later triage.
I have a few things I’d like to explore next – note templates, tag autocomplete, and some refinements to the graph view to make it interactive. These are definitely nice-to-haves, but I would only want more time to think about some of these more thoughtfully as complexity increases with features like interactive graph views. But the core is solid and I’ve been using it daily as my primary note-taking system, which has been the real test.
If you’ve been looking for a note-taking setup in Emacs that doesn’t require much configuration or revamping your notes workflows, give Grove a try! Feedback and contributions are always welcome.
-1:-- Introducing grove.el (Post Jonathan Chu)--L0--C0--2026-05-03T04:00:00.000Z
Philip Kaludercic wanted to continue the conversation from YE24: Sacha and Prot Talk Emacs - Newbies/Starter Kits. He's spent a lot of time thinking about this as one of the main contributors to newcomers-presets, so there'll probably be much to cover!
(America/Toronto -0400) = Thu May 14 1030H EDT / 0930H CDT / 0830H MDT / 0730H PDT / 1430H UTC / 1630H CEST / 1730H EEST / 2000H IST / 2230H +08 / 2330H JST
We'll probably talk about:
Related links:
You can e-mail me at sacha@sachachua.com.
-1:-- May 14: Sacha, Prot, and Philip Kaludercic Talk Emacs: Newcomer Experience (Post Sacha Chua)--L0--C0--2026-05-02T17:21:07.000Z
Sean Whitton writes to say that the Emacs development team is planning to start the Emac 31 release process next week. That means that they will cut the Emacs 31 branch and start the process of testing and refining it in preparation for the final Emacs 31 release.
I really like how the development time is handling releases lately. They plan on a major release about once a year and in between time issue updates to fix bugs or problems in the latest major release.
If you follow the Emacs Devel mailing list, you’d think that the developers’ full time job is Emacs and that they have no family. It’s incredible how much unpaid time these people devote to Emacs
for the benefit of us all. As I always say, if you find yourself in a bar with any of them, the drinks are on you. These guys really are heroes.
-1:-- The Start Of The Emacs 31 Release Process (Post Irreal)--L0--C0--2026-05-02T14:53:37.000Z
Jan G sent me a two-part comment.
I was under the impression that when using elpaca you needed to disable use-package, and that when using elpaca-use-package, you were redefining the macro. I’m not 100% sure about this, but the documentation has an example of use-package and how it actually expands to an elpaca command.
I wouldn't know. All I can say is that it would be nice if package managers that
hook into, or completely redefines use-package, would document if they deviate
from the behaviour of "vanilla use-package" in some way.
Given that, use-package’s documentation is always going to be a little off, since elpaca is doing everything async. The only way I’ve found to reliably manage some dependencies is to use the elpaca-after-init hook, so they don’t even try to run until elpaca is finished loading everything.
I'd say it sometimes seems like the documentation for use-package is a little
off for use-package itself 🙂
The README for Elpaca says that
Add configuration which relies on after-init-hook, emacs-startup-hook, etc to elpaca-after-init-hook so it runs after Elpaca has activated all queued packages.
but that seems like a very big hammer and as I understand it I'd have to move
the whole :init block for python-mode into the hook in that case. Playing
around with the various blocks for use-package isn't too time consuming and I
think it's a good first thing to try.
-1:-- Follow-up on switching to eglot (Post Magnus)--L0--C0--2026-05-02T11:20:00.000Z
I should have dealt with comments I got to my posts on how I deal with secrets in my work notes, here, and here. Better late than never though, I hope.
The first one is a link to post titled How I use :dbconnection in org files. It
describes a nice way of setting sql-connection-alist based on the contents of
a file, in his case ~/.pgppass.
The other starts with a function for searching ~/.authinfo.gpg for entries of
the form
machine <host>/<dbname> login <username> password <password> port <port>
and then setting sql-password-search-wallet-function and sql-password-wallet
to tell sql-mode to use it
(defun my/sql-auth-source-search-wallet (wallet product user server database port)
"Read auth source WALLET to locate the USER secret.
Sets `auth-sources' to WALLET and uses `auth-source-search' to locate the entry.
The DATABASE and SERVER are concatenated with a slash between them as the
host key."
(when-let (results (auth-source-search :host (concat server "/" database)
:user user
:port (number-to-string port)))
(when (and (= (length results) 1)
(plist-member (car results) :secret))
(plist-get (car results) :secret))))
(setq sql-password-search-wallet-function #'my/sql-auth-source-search-wallet)
(setq sql-password-wallet "~/.authinfo.gpg")
The value for sql-connection-alist is then as normal
(setq sql-connection-alist
'((some-dbname (sql-product 'oracle)
(sql-port 1521)
(sql-server ...)
...))
and the blocks in orgmode looks like this
SRC sql-mode :product oracle :dbconnection i3v1e-ro :results raw
SELECT to_char(sysdate, 'YYYY-MM-DD HH24:ii:ss') AS today,
to_char(sysdate + 1, 'YYYY-MM-DD HH24:ii:ss') AS tomorrow
FROM dual;
SRC
Both of these feel closer to the intent of sql-mode in a way. I'll have to try
using sql-connection-alist at some point.
-1:-- Secrets when connecting to DBs (Post Magnus)--L0--C0--2026-05-02T10:41:00.000Z
Raw link: https://www.youtube.com/watch?v=JG4R-d0N-is
In this short video I demonstrate my new package for Emacs. It is called buffer-to-pdf. The idea is to save your current buffer to a PDF, while preserving how it looks. This means that your font size, theme, and other visual effects are preserved and written to the PDF. buffer-to-pdf is not meant to be a replacement for elaborate export methods: consider it a quick yet effective way to get a “screen capture” of your Emacs that you can then share as a document. I believe this will be especially useful for academics or people who need to distribute presentation notes on a regular basis. The package is available here: https://github.com/protesilaos/buffer-to-pdf.
-1:-- Emacs: save any buffer as PDF (my new buffer-to-pdf package) (Post Protesilaos)--L0--C0--2026-05-02T00:00:00.000Z
Here are a couple of quickies. Neither one requires much exposition on my part but they are both interesting. In the first, Prot shares what he considers decent default settings for Emacs. Naturally, I don’t agree with all of them but they’re an excellent start for your init.el. For example, I’d be more likely to prefer Melpa over Gnu Elpa, and I prefer Swiper to Consult but that may be simply because I’m used to Swiper and it works well for me.
Most of the rest of his choices I agree with or at least am agnostic about. That’s the thing about Emacs, you get to choose what works best for you.
The second quickie is a post from Charles Choi that discusses bulk search and replace commands in Emacs. Choi begins with reviewing regular expression syntax. That’s important because some Emacs command use the builtin regexp syntax and others call various versions of grep, which, of course, have their own regexp syntax.
As usual with Choi’s posts there’s a lot there and it requires careful reading but it’s worthwhile. As Choi says, the Emacs bulk search and replace commands make possible workflows that would be harrowing in other editors but they’re also difficult to discover. His post is intended to remedy that.
-1:-- A Couple Of Emacs Quickies (Post Irreal)--L0--C0--2026-05-01T14:48:49.000Z
I previously wrote about useful Emacs commands for reading. This allowed me to use j and k to scroll the window up and down instead of going forward or backward a line.
I am now using meow for my modal editing needs and have been really happy with it. Meow allows for custom states to be defined, but I have been using a different method recently that takes advantage of the built-in normal and motion states.
Meow has a very clever design choice in that when a key is pressed it can translate it to the default emacs binding and call it via a macro. Below is an example of the function meow-next, which I have bound to j. It essentially just executes the key-binding that is stored as the value of meow--kbd-forward-line which happens to be C-n.
(defun meow-next (arg)
"Move to the next line.
Will cancel all other selection, except char selection.
Use with universal argument to move to the last line of buffer.
Use with numeric argument to move multiple lines at once."
(interactive "P")
(unless (equal (meow--selection-type) '(expand . char))
(meow--cancel-selection))
(cond
((meow--with-universal-argument-p arg)
(goto-char (point-max)))
(t
(setq this-command #'next-line)
(meow--execute-kbd-macro meow--kbd-forward-line))))
(defvar meow--kbd-forward-line "C-n"
"KBD macro for command `forward-line'.")
Because meow’s keyboard macro commands are all stored as variables, it is easy for us to overwrite them locally. In the following example I will show how I made a minor mode that lets me have a more enjoyable reading experience.
Firstly, we need to bind the new functions to some obscure key combination. I chose to add these to the global keymap in this case as I want them available across a variety of major-modes.
Then I wrote functions to toggle between the original values of the meow–kbd variables and their overrides.
Finally, I defined a minor mode that can be added to a major-mode’s hook.
(keymap-global-set "M-s-j" 'scroll-up-line)
(keymap-global-set "M-s-k" 'scroll-down-line)
(defvar meow-reading-originals
'((meow--kbd-backward-line . "C-p")
(meow--kbd-forward-line . "C-n")))
(defvar meow-reading-overwrites
'((meow--kbd-backward-line . "M-s-k")
(meow--kbd-forward-line . "M-s-j")))
(defun meow-reading-restore-directions ()
"Restore Meow's direction variables locally."
(dolist (binding meow-reading-originals)
(set (make-local-variable (car binding)) (cdr binding))))
(defun meow-reading-overwrite-directions ()
"Overwrite Meow's direction variables locally."
(dolist (binding meow-reading-overwrites)
(set (make-local-variable (car binding)) (cdr binding))))
(define-minor-mode meow-reading-mode
"Adjust some bindings to make `meow-normal-mode' a better experience for reading modes."
:lighter " reading"
(if meow-reading-mode
(progn
(meow-reading-overwrite-directions)
(setq-local meow-cursor-type-motion nil)
(meow--update-cursor-motion))
(progn (meow-reading-restore-directions)
(kill-local-variable meow-cursor-type-motion)
(meow--update-cursor-motion))))
Here is an example of how you can have the minor mode be loaded for all buffers of a given major mode.
(use-package elfeed
:hook
(elfeed-show-mode . meow-reading-mode))
The reason that I prefer this to making a whole new “reading-state” is that this is still using meow’s default motion-state. I get to take advantage of all of the default key-bindings. One of the reasons that I moved from evil to meow was that I was getting tired of having to bind so many functions myself. With this method, I can essentially say “I want this mode to behave the same as all of the others, just with one or two changes”.
I am also working on a minor mode for lisp modes that takes advantage of smartparens to hopefully create a lispy like environment, but that is still a work in progress.
-1:-- Overriding keybindings with Meow (Post Jiewawa)--L0--C0--2026-05-01T00:00:00.000Z
: Added chapters, transcript, and Prot's defaults.
View in the Internet Archive, watch or comment on YouTube, read the transcript online, download the transcript, or e-mail me.
Here are the settings Prot recommended during our chat.
The Emacs Carnival theme for April 2026 is newbies/starter kits. I chatted with Prot about helping people get into Emacs and also supporting lifelong learning.
Prot had some notes on how he started with Emacs in 2019 in All about switching to Emacs (video blog) | Protesilaos. These notes were just a few months after he started, so his experience was pretty fresh.
In Computing in freedom with GNU Emacs | Protesilaos (2026), he said:
Remember that I started using Emacs without a background in programming. … I learnt the basics within a few days. I started writing my own Emacs Lisp within weeks. And within a year I had my modus-themes moved into core Emacs.
Prot has several projects that might be of interest to many newcomers to Emacs:
M-x load-theme or M-x customize-themes awayI'm also curious about his thoughts on the general Emacs newcomer experience and what we can do to make it better.
He also offers Emacs coaching. I wonder if any newbies have taken advantage of that. There are a few other coaches listed on the EmacsWiki. (Ooh, Emacs buddy, that was neat.)
Other possible topics: Philip suggested the following general themes for the Emacs Carnival:
Prot: But these are basically good defaults based on what I have noticed.
Prot: Another thing that is really common is how do I actually set my fonts, right? Because there are like a million ways to do this as well. And the people are like, okay, but which is the one that I should be using? And of course, when I pick one option, I don't mean to say that this is the right option, but it's just to not be technical about it. Like, okay, just use this and forget about it.
Prot: A few other settings and a few common packages. And at the end of this... Oh, sorry. I have to really make this point.
Prot: Ediff by default is unusable. Out of the box, Ediff is literally unusable. I cannot excuse that. Everything else I can excuse, this is not excusable. Sorry. This is the minimum viable setup for it. Sacha: So maybe that's something to suggest for newcomer presets or maybe even the defaults. Prot: I would say the defaults. This is not a newcomer thing. Basically, if you want to have that default layout, you just have to opt into it. Sorry if I'm offending anyone, but I don't mean to say that. You have to consider the ergonomics of it.
Prot: I haven't tried this but what I mean if you do this: mapc disable-theme right, the custom enabled theme maybe you have seen this right so you want to disable all the other themes before loading your theme right I'm sure somebody has written something like this maybe I have done it and then it's like you know load your favorite theme now right and then you do your favorite theme or whatever For example, here. So in this case, I don't know what happens to the newcomers theme. I will assume that it will disable it. In which case, I think that has to be prevented. Sacha: Oh, but then it wouldn't be treated the same as other things. Prot: Which you can do. Which you can do, for example, if I go to Fontaine. And of course, I got this from use-package. But you can do it with a synthetic theme. So there is a little trick you can do.
Prot: Okay, so this is for you. It's like too much work, but I must say. This looks like arcane knowledge but this sort of thing actually is a quality of life improvement to your Emacs because one thing that I think is bad about the default Emacs experience is uncertainty about where things will show up. Like, you never know. Like, you cannot predict it. Because Emacs tries to be sensible about it or whatever, but you cannot predict it. Whereas things that are ancillary should have kind of a more predictable behavior. You can comment on Mastodon or e-mail me at sacha@sachachua.com.
-1:-- YE24: Sacha and Prot Talk Emacs - Newbies/Starter Kits (Post Sacha Chua)--L0--C0--2026-04-30T19:57:50.000Z
-1:-- Emacs Carnival: History and Thoughts on Starter Kits (Post Eric MacAdie)--L0--C0--2026-04-30T17:54:45.000Z
After reading the sad news that Chris Wellons is abandoning Emacs and the Emacs packages he developed one of my first concerns was for Elfeed. It’s the RSS reader that I depend on everyday for my research and news curation. Wellons will be missed for many reasons but his stewardship of Elfeed is the most important one to me.
I’m all but certain that Wellons will find good hands in which to entrust the future of Elfeed but as I learned in the Boy Scouts, it pays to be prepared. Serendipitously, I stumbled upon this post from Dave’s Blog that mentions he moved from Elfeed to Gnus. The post is about a solution he found for following links referred to in the RSS summary but I was more interested in his use of Gnus in place of Elfeed for RSS.
I’ve considered, off and on, moving to Gnus but I’m really happy with mu4e for email and elfeed for RSS and Network News is, sadly, no longer important so moving to Gnus didn’t make much sense to me. Now, unfortunately, I may have to think about it. Gnus is a powerful complicated program and it seems a shame to use it just for RSS but if worse comes to worst it’s nice to have a mature and reliable fallback available.
As I said, I’m pretty sure Elfeed won’t have any problems finding a new maintainer who will sustain its high quality but it’s nice to know that if I need it, there’s a viable, Emacs-based solution available.
-1:-- RSS With Gnus (Post Irreal)--L0--C0--2026-04-30T17:07:01.000Z
I have been doing a lot of fiddling with images lately, mostly through dired and image-dired, and one little thing has been bugging me for a while. When I open an image in Emacs, image-mode happily shows me the picture, but it never tells me the one bit of information I actually want to know, how big is the thing?, width, height, file size, that sort of thing. You can of course bounce out to a shell and run identify or file, but that feels silly when Emacs already has the image loaded.
So I thought, right, this should be a five minute job, just slap something into the header-line on image-mode-hook and be done with it. And it more or less was, although there was a small wrinkle along the way that is worth mentioning, because it caught me out.
Here is what ended up in my init.el:
;; ;; -> image-mode-dimensions ;; (defun my/image-mode-show-dimensions () "Display the open image's pixel dimensions and file size in the header line." (when (and (derived-mode-p 'image-mode) buffer-file-name (file-exists-p buffer-file-name)) (condition-case err (let* ((image (or (image-get-display-property) (create-image buffer-file-name))) (size (image-size image t)) (width (car size)) (height (cdr size)) (bytes (file-attribute-size (file-attributes buffer-file-name)))) (setq header-line-format (format " %d x %d px %s" width height (file-size-human-readable bytes)))) (error (setq header-line-format (format " image dimensions unavailable: %S" err)))))) (add-hook 'image-mode-hook #'my/image-mode-show-dimensions) (add-hook 'image-mode-new-window-functions (lambda (&rest _) (my/image-mode-show-dimensions)))
A few notes on what is going on here. The or around image-get-display-property and create-image means we use the displayed image spec when it is there (cheaper, no extra file read), and fall back to building one from the file path when it is not. image-size with a non-nil second argument returns dimensions in actual pixels rather than canvas units, which is what I want. file-size-human-readable gives me a nice 2.4M rather than 2516582, because nobody reads bytes directly.
The condition-case is there because images can occasionally throw, especially when something has gone wrong with imagemagick or an unsupported format sneaks in, and I would rather see a polite header-line message than have the hook explode and pollute *Messages* every time I open a picture.
The second hook, image-mode-new-window-functions, is the one that handles the case where you flip between image and text view of the same buffer with C-c C-c, since the image gets re-displayed and the header-line needs to refresh too.
So now when I pop open an image, I get a nice little header-line that reads something like:
1920 x 1080 px 2.4M
A small thing, but the kind of small thing that makes Emacs feel like it fits a bit more snugly around how I actually work.
-1:-- A Tiny Header line Tweak: Image Dimensions in image mode (Post James Dyer)--L0--C0--2026-04-30T10:00:00.000Z
Emacs offers a dizzying number of commands to perform bulk operations on multiple files or buffers. These commands are quite capable and can make trivial the execution of workflows that would be heroic in other editors. Unfortunately these commands are also difficult to discover as a number of them are not featured in easily accessible menus.
This post calls attention to bulk commands related to search and replace in Emacs and how their discoverability can be improved using the Casual and Anju UI packages.
Before moving forward, let’s talk about how Emacs handles regular expression syntax in search and/or replace commands. Whenever using an Emacs command involving a regular expression (regexp), you will need to understand what kind of regexp syntax is expected. There are largely two to know:
Commands using grep-style regexps do so because they invoke the local install of the grep utility. The grep regex syntax is implementation dependent. If you use GNU grep, you can read its syntax here, which share some character classes with Emacs.
The Casual RE-Builder package is a great way to work with Emacs regexps as it provides a command to escape the regexp properly for interactive input. re-builder alone only provides a regexp that can be used in Elisp code.
The table below shows the different search and/or replace workflows and their associated commands.
| Workflows | Command | Regexp Type | Notes |
|---|---|---|---|
| Search regexp in files in directory | rgrep | grep | AKA “Find in Files.” Results can be edited using writeable grep (wgrep). |
| Search regexp in files in project | project-find-regexp | grep | |
| Search regexp in files in directory and present in Dired | find-grep-dired | grep | |
| Search regexp in tagged files | tags-search | Emacs | |
| Search file names matching shell pattern in directory and present in Dired | find-name-dired | shell | |
| Search file names matching regexp in directory and present in Dired | find-lisp-find-dired | Emacs | Implementation uses Emacs regexp type. |
| Search file names matching regexp in current directory and present in Dired | casual-dired-find-dired-regexp | Emacs | Wrapper around find-lisp-find-dired using current directory. Available via Casual. |
| Query replace regexp in project files | project-query-replace-regexp | Emacs | |
| Incremental search marked files with plain text | dired-do-isearch | For use in Dired. | |
| Incremental search marked files with regexp | dired-do-isearch-regexp | Emacs | For use in Dired. |
| Query replace regexp in marked files | dired-do-query-replace-regexp | Emacs | For use in Dired. |
| Search regexp in marked files | dired-do-find-regexp | grep | For use in Dired. |
| Search regexp in marked files and display first match | dired-do-search | Emacs | For use in Dired. |
| Replace regexp in marked files | dired-do-find-regexp-and-replace | grep | For use in Dired. |
| Replace regexp in marked files using diff interface. | dired-do-replace-regexp-as-diff | Emacs | For use in Dired. |
| Incremental search marked buffers with plain text | ibuffer-do-isearch | For use in IBuffer. | |
| Incremental search marked buffers with plain text with regexp | ibuffer-do-isearch-regexp | Emacs | For use in IBuffer. |
| View lines which match regexp in marked buffers | ibuffer-do-occur | Emacs | For use in IBuffer. |
| Query replace plain text in marked buffers | ibuffer-do-query-replace | For use in IBuffer. | |
| Query replace regexp in marked buffers | ibuffer-do-query-replace-regexp | Emacs | For use in IBuffer. |
| Run git grep, searching for regexp in directory. | vc-git-grep | grep | |
| Replace all references to identifier. | xref-find-references-and-replace | ||
| Interactively replace identifier in current xref buffer. | xref-query-replace-in-results | ||
| Rename symbol at point for project. | eglot-rename | Requires Eglot support. | |
| Replace regexp in files using diff interface. | multi-file-replace-regexp-as-diff | Emacs | |
| Start multi-buffer incremental search on a list of files. | multi-isearch-files | ||
| Start multi-buffer incremental regexp search on a list of files. | multi-isearch-files-regexp | Emacs | |
| Start multi-buffer incremental search on a list of buffers. | multi-isearch-buffers | ||
| Start multi-buffer incremental regexp search on a list of buffers. | multi-isearch-buffers-regexp | Emacs | |
| Show all lines in buffers containing a match for regexp. | multi-occur | Emacs | |
| Show all lines containing a match for regexp in buffers that match bufregexp. | multi-occur-in-matching-buffers | Emacs |
Choice of command depends on the quantity and specificity of files or buffers to work on. Both Dired and IBuffer marking allow for specific selection of files or buffers respectively.
Many of the commands in the table above are discoverable via menu using the Casual and Anju packages. Hierarchical menu categorization aids in the discovery and recognition of these commands, making them more usable.
| Command | UI | Notes |
|---|---|---|
| rgrep | (C-o) casual-editkit-main-tmenu › (/) Search/Replace › (g) Find in Files… | Available via Casual EditKit. |
| rgrep | (Menu-bar) Edit › Search › Search in Files… | Available via Anju. |
| project-find-regexp | (Menu-bar) Edit › Search › Search in Project Files… | |
| tags-search | (Menu-bar) Edit › Search › Search Tagged Files… | |
| project-query-replace-regexp | (Menu-bar) Edit › Replace › Replace in Project Files… | |
| find-name-dired | (C-o) casual-editkit-main-tmenu › (/) Search/Replace › (d) Files… | Available via Casual EditKit. |
| find-grep-dired | (C-o) casual-editkit-main-tmenu › (/) Search/Replace › (G) Files containing text… | Available via Casual EditKit. |
| casual-dired-find-dired-regexp | (C-o) casual-dired-tmenu › (f) Filter by name… | Available via Casual Dired. |
| dired-do-isearch | (C-o) casual-dired-tmenu › (/) Search/Replace › (C-s) I-search… | Available via Casual Dired. |
| dired-do-isearch-regexp | (C-o) casual-dired-tmenu › (/) Search/Replace › (M-s) I-search regexp… | Available via Casual Dired. |
| dired-do-query-replace-regexp | (C-o) casual-dired-tmenu › (/) Search/Replace › (r) Query regexp and replace… | Available via Casual Dired. |
| dired-do-find-regexp | (C-o) casual-dired-tmenu › (/) Search/Replace › (g) Find regex… | Available via Casual Dired. |
| dired-do-search | (C-o) casual-dired-tmenu › (/) Search/Replace › (s) Search first regexp match… | Available via Casual Dired. |
| dired-do-find-regexp-and-replace | (C-o) casual-dired-tmenu › (/) Search/Replace › (G) Find regex and replace… | Available via Casual Dired. |
| ibuffer-do-isearch | (C-o) casual-ibuffer-tmenu › (C-s) I-Search… | Available via Casual IBuffer. |
| ibuffer-do-isearch-regexp | (C-o) casual-ibuffer-tmenu › (C-M-s) I-Search Regexp… | Available via Casual IBuffer. |
| ibuffer-do-occur | (C-o) casual-ibuffer-tmenu › (O) Occur… | Available via Casual IBuffer. |
| ibuffer-do-query-replace | (C-o) casual-ibuffer-tmenu › (M-r) Query Replace… | Available via Casual IBuffer. |
| ibuffer-do-query-replace-regexp | (C-o) casual-ibuffer-tmenu › (C-M-r) Query Replace Regexp… | Available via Casual IBuffer. |
| vc-git-grep | (Menu-bar) Tools › Version Control > Git grep… |
The replace commands can do catastrophic damage if not used with caution. Treat them like power tools.
Some guidance before using replace commands:
Have a backup plan in case you need to recover anything.
Use re-builder to help figure out the right regexp.
Know the scope of what you want to change (directory, files, buffers).
Some commands will only modify the buffer of an affected file. Check if modified buffers are saved. You can use IBuffer to identify and save such buffers. It is left as an exercise to the reader to determine which commands exhibit this behavior.
The inventory of commands above are from what I know of Emacs, which is guaranteed to not be comprehensive. If there are others missing from this list, please let me know at kickingvegas@gmail.com.
While many of these commands have been in Emacs for years (decades even), they were not usable to me until I built a UI for them. I think motivated readers using Casual and Anju for bulk search and/or replace tasks will find a similar sentiment.
Amended 2026-04-30: Added vc-git-grep, eglot-rename, xref-find-references-and-replace, and xref-query-replace-in-results. Thanks to Henry Leach and Gene Pasquet for their input!
Amended 2026-05-04: Added dired-do-replace-regexp-as-diff, multi-file-replace-regexp-as-diff, multi-isearch-files, multi-isearch-files-regexp, multi-isearch-buffers, multi-isearch-buffers-regexp, multi-occur, multi-occur-in-matching-buffers. Learned about the diff commands from Christian Tietze's post Preview Mass Text Replacements with Emacs 30.1 replace-regexp-as-diff.
-1:-- Bulk Search & Replace Commands for Files and Buffers in Emacs (Post Charles Choi)--L0--C0--2026-04-30T05:15:00.000Z
These are the basic settings for Emacs that I shared with Sacha Chua during our livestreamed meeting on 2026-04-30: https://www.youtube.com/watch?v=z7pcLdwuyxE.
UPDATE 2026-05-01 08:24 +0300: Added a missing :config to the
bookmark block.
UPDATE 2026-05-02 22:33 +0300: Replaced duplicate variable-pitch
with the intended fixed-pitch.
;;; Sensible defaults that are not too intrusive and focus on common use-cases. By Protesilaos on 2026-04-30.
;; These are not all of my favourite options. I am not even including
;; any of my packages. They are just some basics that I consider
;; useful, given what I have learnt from my exchange with other people
;; of all skill levels.
;; Persist all customisations in a separate file called "custom.el".
;; It is in the same directory as the "init.el".
;;
;; Without the `custom-file', Emacs writes directly to the "init.el",
;; which can be confusing.
(setq custom-file (locate-user-emacs-file "custom.el"))
(load custom-file :no-error-if-file-is-missing)
(use-package package
:ensure nil
:config
;; I am not using `add-to-list' here because the default "gnu" is
;; confusing to people, given that "elpa" is the better known name
;; for it.
(setq package-archives
'(("gnu-elpa" . "https://elpa.gnu.org/packages/")
("nongnu" . "https://elpa.nongnu.org/nongnu/")
("melpa" . "https://melpa.org/packages/")))
;; Prefer GNU ELPA but accept the reality of MELPA's utility to the
;; wider community.
(setq package-archive-priorities
'(("gnu-elpa" . 3)
("nongnu" . 2)
("melpa" . 1))))
;;;; General options
(use-package emacs
:ensure nil
:demand t
:init
(defun prot/keyboard-quit-dwim ()
"Do-What-I-Mean behaviour for a general `keyboard-quit'.
The generic `keyboard-quit' does not do the expected thing when
the minibuffer is open. Whereas we want it to close the
minibuffer, even without explicitly focusing it.
The DWIM behaviour of this command is as follows:
- When the region is active, disable it.
- When a minibuffer is open, but not focused, close the minibuffer.
- When the Completions buffer is selected, close it.
- In every other case use the regular `keyboard-quit'."
(interactive)
(cond
((region-active-p)
(keyboard-quit))
((derived-mode-p 'completion-list-mode)
(delete-completion-window))
((> (minibuffer-depth) 0)
(abort-recursive-edit))
(t
(keyboard-quit))))
:bind
("C-g" . prot/keyboard-quit-dwim)
:config
;; Set your favourite font family and height here. The :height is
;; 10x the point size you most commonly find on other applications.
(set-face-attribute 'default nil :family "Aporetic Sans Mono" :height 160)
;; Set your favourite font for elements that are designed to always
;; be monospaced. The height SHOULD BE a floating point, which is
;; interpreted as relative to the `default'.
(set-face-attribute 'fixed-pitch nil :family "Aporetic Serif Mono" :height 1.0)
;; Same as above for proportionately spaced elements. Make any
;; buffer proportionately spaced by enabling the `variable-pitch-mode'.
;;
;; [ NOTE: If you use the Modus themes or derivatives, set
;; `modus-themes-mixed-fonts', load the theme for the option to
;; take effect, and then enable `variable-pitch-mode':
;; spacing-sensitive elements like Org tables and code blocks will
;; remain monospaced. ]
(set-face-attribute 'variable-pitch nil :family "Aporetic Sans" :height 1.0)
;; I have never seen a user say "no" to loading a theme they have
;; downloaded. Technically, any Elisp file can run arbitrary code,
;; so this is not doing much on the security front.
(setq custom-safe-themes t)
(setq use-short-answers t)
(setq read-answer-short t)
(setq help-window-select t) ; also check `display-buffer-alist' below
(setq help-window-keep-selected t) ; Emacs 29
(setq find-library-include-other-files nil) ; Emacs 29
(setq window-combination-resize t)
(setq save-interprogram-paste-before-kill t)
;; Do not jump to the current line in `*occur*' buffers. The reason
;; is that you are already on that line: you want to do `occur' to
;; get more than that (and, presumably, to do something with the
;; results such as to edit them with `occur-edit-mode').
(setq list-matching-lines-jump-to-current-line nil)
(setq completion-category-defaults nil))
;;;; Save minibuffer histories
(use-package savehist
:ensure nil
:config
(savehist-mode 1))
;;;; Delete the selected text when inserting new text
(use-package delsel
:ensure nil
:config
(delete-selection-mode 1))
;;;; Bookmarks
(use-package bookmark
:ensure nil
:config
;; Emacs 29 displays a bookmark icon on the fringe. Many people
;; have asked me what that thing is. I also think it is confusing.
(setq bookmark-fringe-mark nil)
;; Write changes to the bookmark file as soon as 1 modification is
;; made (addition or deletion). Otherwise Emacs will only save the
;; bookmarks when it closes, which may never happen properly
;; (e.g. power failure).
(setq bookmark-save-flag 1))
;;;; Dired
(use-package dired
:ensure nil
:config
;; Most people I have talked to prefer a single Dired buffer.
;; Personally I like the many Dired buffers, but I understand why
;; this feels overwhelming.
(setq dired-kill-when-opening-new-dired-buffer t)
(setq dired-auto-revert-buffer #'dired-directory-changed-p) ; also see `dired-do-revert-buffer'
(setq dired-clean-up-buffers-too t)
(setq dired-clean-confirm-killing-deleted-buffers t)
(setq dired-recursive-copies 'always)
(setq dired-recursive-deletes 'always)
(setq delete-by-moving-to-trash t)
(setq dired-create-destination-dirs 'ask)
(setq dired-create-destination-dirs-on-trailing-dirsep t) ; Emacs 29
(setq wdired-create-parent-directories t))
;;;; Isearch
(use-package isearch
:ensure nil
:config
;; ;; Enable those to make "package install" match those words with
;; ;; anything in between. I think this is the single best tweak I
;; ;; ever made.
;;
;; (setq search-whitespace-regexp ".*?")
;; (setq isearch-lax-whitespace t)
;; (setq isearch-regexp-lax-whitespace nil)
(setq isearch-lazy-count t)
(setq lazy-count-prefix-format "(%s/%s) ")
(setq lazy-count-suffix-format nil))
;;;; Diff
(use-package diff
:ensure
:config
;; You cannot expect the syntax highlighting of themes to look
;; equally readabable against what typically are red and green
;; backgrounds. This should be opt-in by default, not opt-out.
(setq diff-font-lock-syntax nil))
;;;; Ediff
(use-package ediff
:ensure nil
:config
;; Ediff is virtually unusable without those. Especially on tiling
;; window managers. But even on a regular desktop environment it is
;; confusing and cumbersome to have the control panel in another
;; frame.
(setq ediff-split-window-function 'split-window-horizontally)
(setq ediff-window-setup-function 'ediff-setup-windows-plain))
;;;; SHR
(use-package shr
:ensure nil
:config
;; t is bad for accessibility and generally awkward for HTML email
;; (especially with dark themes).
(setq shr-use-colors nil)
;; This option should not exist, given `variable-pitch-mode'.
;; Furthermore, its default value runs counter to almost everything
;; else in Emacs which just uses the `default' face.
(setq shr-use-fonts nil))
;;;; Control the display of common ancillary windows
;; Always focus common ancillary windows. Place them in a window
;; already occupied by their respective major mode or below the
;; current window.
(add-to-list 'display-buffer-alist
'((or . ((derived-mode . occur-mode)
(derived-mode . grep-mode)
(derived-mode . Buffer-menu-mode)
(derived-mode . log-view-mode)
(derived-mode . help-mode)))
(display-buffer-reuse-mode-window display-buffer-below-selected)
(body-function . select-window)))
(add-to-list 'display-buffer-alist
'("\\`\\*\\(Org \\(Select\\|Note\\)\\|Agenda Commands\\)\\*\\'" ; the `org-capture' key selection, `org-add-log-note', and agenda dispatcher
(display-buffer-in-side-window)
(dedicated . t)
(side . bottom)
(slot . 0)
(window-parameters . ((mode-line-format . none)))))
(add-to-list 'display-buffer-alist
'((derived-mode . calendar-mode)
(display-buffer-reuse-mode-window display-buffer-below-selected)
(mode . (calendar-mode bookmark-edit-annotation-mode ert-results-mode))
(inhibit-switch-frame . t)
(dedicated . t)
(window-height . fit-window-to-buffer)))
(add-to-list 'display-buffer-alist
'((derived-mode . reb-mode) ; M-x re-builder
(display-buffer-reuse-mode-window display-buffer-below-selected)
(inhibit-switch-frame . t)
(window-height . 4) ; note this is literal lines, not relative
(dedicated . t)
(preserve-size . (t . t))))
;;;; ESSENTIAL packages to install
(use-package vertico
:ensure t
:config
(vertico-mode 1))
(use-package marginalia
:ensure t
:config
(marginalia-mode 1))
;;;; VERY USEFUL but not essential packages
(use-package orderless
:ensure t
:config
(setq completion-styles '(orderless basic)))
(use-package consult
:ensure t
;; All commands have their utility, but those are commonly needed.
:commands (consult-buffer consult-line consult-outline consult-find consult-grep))
(use-package embark
:ensure t
:bind
;; Embark is helpful in every context, though there are other ways
;; to do what it does. Where it stands out is in its ability to
;; deal with all the minibuffer results. The equivalent of those
;; two commands should be a core Emacs functionality.
( :map minibuffer-local-map
("C-c C-c" . embark-collect)
("C-c C-e" . embark-export))
:config
;; Needed for correct exporting while using Embark with Consult commands.
(use-package embark-consult
:ensure t
:after consult))
;; Useful when combined with `delete-by-moving-to-trash'.
(use-package trashed
:ensure t)
-1:-- Emacs: decent defaults I shared with Sacha Chua (Post Protesilaos)--L0--C0--2026-04-30T00:00:00.000Z
I recently opened my feed to discover this post from Chris Wellons. The TL;DR is that Wellons, after 20 years, is abandoning Emacs for Vim. Folks do that, of course, but Wellons is special because he’s been a prolific author of Emacs packages including the absolute best RSS reader, Elfeed that I use everyday and depend on for my Irreal research.
Over the years, I’ve interacted with Wellons several times and always found him to be personable, engaged, and shockingly smart. He’s exactly what you want in an interlocutor: informed but not inextricably bound to his opinions. He was always interested in the truth and willing to be persuaded by facts.
By now, this has been all over the Internet and Sacha has already covered it on Emacs News but I wanted to add my appreciation for Wellons and everything he’s done for the Emacs community. I and the rest of our community can only hope that he returns from the dark side but in any event, we wish him well and thank him for all he’s done for us.
In the meantime, he still has some packages in need of a new maintainer. There’s a list on his post. If you’re interested, get in touch with him.
-1:-- Goodbye Old Friend (Post Irreal)--L0--C0--2026-04-29T15:09:06.000Z
I’ve been reading RSS and Atom feeds in Gnus for a few weeks, having moved over from elfeed. IIRC in elfeed it was pretty easy to visit the URL that an entry summarized. I’ve been searching for a while to do the same in Gnus, without much luck.
But today I finally hit C-h b while in the Gnus summary buffer, and
finally noticed w bound to
gnus-summary-browse-url. Aha! This function will “Scan
the current article body for links, and offer to browse them.” For
many articles, this will get me the one and only link, to the
article. For some of the articles, this finds several URLs, so I just
have to try to pick the right one. For that, I note
If only one link is found, browse that directly, otherwise use completion to select a link. The first link marked in the article text with ‘gnus-collect-urls-primary-text’ is the default.
And what is gnus-collect-urls-primary-text? It’s “The
button text for the default link in ‘gnus-summary-browse-url’” and its
value is “Link”. Exactly what I’m usually seeking!
This is an example of Emacs being self-documenting.
-1:-- Browsing URLs from Gnus Summary buffer (Post Dave's blog)--L0--C0--2026-04-29T00:00:00.000Z
For a while now, the python shell in Python mode in Emacs has not worked properly on my work laptop. I get some strange glyphs in the top of the python shell buffer when opening it from a file with C-c C-p and when I tried importing a file with C-c C-c, I would get an error saying stringp was nil and every character in what I tried to import was written first once, then together with the next, then the next three and so on. It looked somewhat psychedelic and was more or less unusable.
I also had trouble evaluating python code blocks in org mode which has been annoying when presenting with inter-present-mode for my students or exporting from org files I used for presentations to PDF or docx to upload to our school's learning platform (it's learning). In org, I would get the same error saying stringp was nil when evaluating code blocks that I would get when trying to import files to the REPL in python mode.
I first thought the problem was related to my einar-python-virtualenv function which locally changes the python-shell-interpreter for a file if it is in a project with a virtual environment since I made that quite recently, but the strange thing was that it worked perfectly on GNU Guix on my main machine at home. I then thought maybe it was a Windows 11 problem since I only experienced it at work, so I tried on a Raspberry Pi 4 at work with the latest Raspberry Pi OS and I had the same problem.
I then tried launching Emacs with -q to see if the problem was related to my config or not. When I then launched the python shell from a file with C-c C-p, the REPL itself worked slightly better, but I still got some strange glyphs at the top. I then spent some time trying to figure out where in my config the problem appeared and removed first the einar-python-virtualenv and then my configuration for python-mode, but the problem persisted. I also looked into whether some setting for comint-mode might cause the problem.
After spending one and a half hour trying to troubleshoot this on Monday, I today realised I updated Python to 3.14 on my work Windows 11 laptop a while ago. I checked the python version on the Raspberry Pi and it was 3.13. I checked packages.guix.gnu.org and Guix uses version 3.11. I thought maybe something changed in one of the newer versions. I had a look at news for version 3.14 and did not find anything promising. I then checked out what was new in 3.13 and one of the first things was a new and improved python shell.
Luckily, you can get the old shell that python mode in Emacs works with by setting the environment variable PYTHON_BASIC_REPL to something. When I added this function call to my Emacs config for python mode, the python REPL works in python mode again and it is possible to evaluate org code blocks and export files that will evaluate them on export again:
(setenv "PYTHON_BASIC_REPL" "1")
I write this so others experiencing the same problem can find a solution. In the long term, python-mode, python-ts-mode and org mode needs to work with the new and improved python shell since the old shell will probably be removed at some point in the future, but for now, just setting the environment variable is enough to get both the REPL and org code blocks working again.
-1:-- Fix Emacs python-mode REPL and org code block with python evaluation problems (Post Einar Mostad)--L0--C0--2026-04-28T18:55:00.000Z
Figure 1: JPEG produced with DALL-E 3
This is the fourth post in a series on Emacs completion. The first argued that Incremental Completing Read (ICR) is a structural property of an interface rather than a convenience feature. The second broke the Emacs substrate into eight packages (collectively VOMPECCC) each solving one of the six orthogonal concerns of a complete completion system. The third walked through spot, a ~1,100-line Spotify client built as a little shim on top of those packages.
This post is the hands-on complement to the spot post. Where the spot case study reviewed a finished codebase from the outside, this one builds a tiny produce picker tool from scratch, one VOMPECCC package at a time. The use case is deliberately trivial: we have a list of produce items (twenty fruits and ten vegetables) with some metadata, and we want to pick one and do something with it.
Every piece of interesting behavior; the display control, multi-component matching, metadata columns, narrowing keys, contextual actions, transformer-driven type refinement, frecency-based sorting; will be layered on by adding one VOMPECCC package at a time, in order to make it clear exactly what each package provides. By the end, we will have a ~90-line produce picker whose entire UI was composed from six packages that don't know about each other.
We will build a ~90-line produce picker whose entire UI was composed from six packages that don't know about each other.
A caveat: two of the eight VOMPECCC packages are out of scope here. Corfu and Cape target in-buffer completion, and the produce picker in this post is focused on minibuffer interaction. So this post focuses on the six packages that do show up visibly in a live walkthrough: Vertico, Orderless, Marginalia, Prescient, Embark, and Consult.
A note on format. You can open this webpage in EWW and execute the code from within there (you can see my video for an example of how to do this).
This video demo walks through the code in this post live. I have also included prose in each section explaining what each piece of code does. The article is long, so the video will likely be a quicker digestion of this post's message.
Note: wherever you see video demos, you will see, in the upper right hand side (in the tab-bar), the keybindings and associated commands that I am invoking to execute each command. This is relevant because you may have things configured differently on your side. By providing both the kbd and command name, my invocation of behaviors you see in the video should unambiguous.
The data is a list of candidates, which in this implementation are propertized strings, one per produce item. Each candidate is the produce item's name, with five text properties riding along: category, type, color, season, and price. Two of those properties are load-bearing for later phases. category is the completion category, the symbol Marginalia and Embark dispatch on.
A word on the name category, because it is doing more work than it looks like. Of the five property names above, four are arbitrary: type, color, season, price are labels we chose, nothing in Emacs reserves them, and you could rename them all and only have to update our own code. category is the exception because it is a reserved text-property name in Emacs. The Elisp manual specifies that when a character has a category text property whose value is a symbol, that symbol's property list serves as defaults for the character's other text properties. In other words, category is Emacs's standard hook for stamping a typed symbol onto a piece of text. Emacs's completion ecosystem overloads the same name with a second, related meaning, which is that every prompt has a completion category (a symbol like file, buffer, or in our case fruit or vegetable), which Marginalia, Embark, etc… consult through the completion metadata to decide which annotator, keymap, or exporter to dispatch.
There is one important subtlety worth surfacing now, since it explains a lot of what happens later. The framework never reads our category text property directly. Marginalia and Embark dispatch off the prompt-level completion metadata (a separate channel from text properties), and Consult, when we add it in Phase 5, communicates per-candidate categories through a different text property called multi-category rather than ours. So our category property is read only by our own code: a corpus-filtering helper in Phase 4, an Embark exporter in Phase 6, etc…. The framework dispatches because we set the Consult source's :category key to match the data's category property by hand. The two stay in sync because we keep them in sync, not because anyone cross-checks. This is the candidate-as-currency convention being load-bearing for us, with the framework reading a parallel, framework-owned slot for its own dispatch.
type is the finer classification (botanical: pome, berry, citrus, stone, tropical, melon; vegetable: root, leafy, cruciferous, nightshade) used in Phase 6 to drive an Embark transformer that gives citrus its own action set. Both classifications live on the candidate itself. No framework code in any phase below ever invents a category or hardcodes a type, and the routing keys are pulled directly off the candidates.
(defvar my-produce (list ;; Fruits (propertize "apple" 'category 'fruit 'type 'pome 'color "red" 'season "fall" 'price 1.29) (propertize "pear" 'category 'fruit 'type 'pome 'color "green" 'season "fall" 'price 1.79) (propertize "strawberry" 'category 'fruit 'type 'berry 'color "red" 'season "spring" 'price 3.99) (propertize "blueberry" 'category 'fruit 'type 'berry 'color "blue" 'season "summer" 'price 4.99) (propertize "raspberry" 'category 'fruit 'type 'berry 'color "red" 'season "summer" 'price 5.99) (propertize "blackberry" 'category 'fruit 'type 'berry 'color "black" 'season "summer" 'price 5.49) (propertize "lemon" 'category 'fruit 'type 'citrus 'color "yellow" 'season "year-round" 'price 0.79) (propertize "lime" 'category 'fruit 'type 'citrus 'color "green" 'season "year-round" 'price 0.39) (propertize "orange" 'category 'fruit 'type 'citrus 'color "orange" 'season "winter" 'price 0.99) (propertize "grapefruit" 'category 'fruit 'type 'citrus 'color "pink" 'season "winter" 'price 1.49) (propertize "peach" 'category 'fruit 'type 'stone 'color "orange" 'season "summer" 'price 2.49) (propertize "plum" 'category 'fruit 'type 'stone 'color "purple" 'season "summer" 'price 2.99) (propertize "cherry" 'category 'fruit 'type 'stone 'color "red" 'season "summer" 'price 6.99) (propertize "apricot" 'category 'fruit 'type 'stone 'color "orange" 'season "summer" 'price 3.99) (propertize "mango" 'category 'fruit 'type 'tropical 'color "orange" 'season "summer" 'price 1.99) (propertize "pineapple" 'category 'fruit 'type 'tropical 'color "yellow" 'season "year-round" 'price 3.99) (propertize "banana" 'category 'fruit 'type 'tropical 'color "yellow" 'season "year-round" 'price 0.59) (propertize "papaya" 'category 'fruit 'type 'tropical 'color "orange" 'season "year-round" 'price 2.49) (propertize "watermelon" 'category 'fruit 'type 'melon 'color "green" 'season "summer" 'price 0.59) (propertize "cantaloupe" 'category 'fruit 'type 'melon 'color "orange" 'season "summer" 'price 2.99) ;; Vegetables (propertize "carrot" 'category 'vegetable 'type 'root 'color "orange" 'season "year-round" 'price 0.99) (propertize "beet" 'category 'vegetable 'type 'root 'color "purple" 'season "fall" 'price 1.49) (propertize "radish" 'category 'vegetable 'type 'root 'color "red" 'season "spring" 'price 0.79) (propertize "spinach" 'category 'vegetable 'type 'leafy 'color "green" 'season "spring" 'price 2.99) (propertize "kale" 'category 'vegetable 'type 'leafy 'color "green" 'season "winter" 'price 2.49) (propertize "lettuce" 'category 'vegetable 'type 'leafy 'color "green" 'season "summer" 'price 1.99) (propertize "broccoli" 'category 'vegetable 'type 'cruciferous 'color "green" 'season "year-round" 'price 2.99) (propertize "cauliflower" 'category 'vegetable 'type 'cruciferous 'color "white" 'season "fall" 'price 3.99) (propertize "tomato" 'category 'vegetable 'type 'nightshade 'color "red" 'season "summer" 'price 2.49) (propertize "eggplant" 'category 'vegetable 'type 'nightshade 'color "purple" 'season "summer" 'price 3.49)) "Produce candidates (fruits + vegetables) for the VOMPECCC walkthrough.")
Thirty propertized strings, two completion categories (fruit, vegetable), ten types (six botanical + four vegetable), and four additional properties. For the rest of this post, produce candidate means one of these propertized strings: a plain Emacs string at the surface (the produce name, if you will) with its remaining fields as text properties.
The post is going to lean on one specific claim: the candidate is the unit of currency that flows between every layer of the substrate. When we say "every package consumes the same currency without knowing about each other," we mean the propertized string above is what gets used by the built-in Emacs substrate and the VOMPECCC packages. The shape we chose for my-produce is what makes the thesis cash out.
It is worth pausing on, because the same data could plausibly have been a list of plists, an alist of cons cells, a hash table from name to record, or a programmed completion function, etc…. and completing-read accepts all of those as collections. No VOMPECCC package constrains the shape further, so strictly speaking, any shape that produces strings would work. The question is what each shape costs the consumer code, and the answer is relevant to the rest of this post.
1. Properties survive the round trip. When completing-read returns the chosen candidate, it returns the exact propertized string you put in. Properties intact. Your Embark action receives "apple" with all its text properties; your transformer receives "apple" with type pome still attached; the exporter receives the full set. The entire candidate-as-currency story rests on this: domain data and candidate identity are the same object. You hand the framework a propertized string, and you get a propertized string back, and you can read whatever properties you stamped on without ever leaving the candidate.
2. Text properties are the framework's integration channel. Three concrete examples from the packages below:
face text properties for display.multi-category text property to thread per-candidate types through Embark's dispatcher.multi-category off the candidate.
These packages were designed assuming candidates are strings with rich text properties. The two framework-owned properties (face and multi-category) coexist on the same string object alongside our category, type, color, season, price, because a propertized string supports arbitrary properties without conflict. An alist or hash-table shape would force you to translate to strings somewhere on the way into the framework, and that breaks the integration, or at least makes it more difficult.
3. No sidecar state. The plausible alternative is plain strings plus a hash table mapping name → record. It works, but introduces:
(gethash cand my-records) step in every annotator, action, transformer, and exporter. This is 'lookup' or 'rehydration'. Best avoided, and I can only think of this being useful if a candidate has a huge number of mostly unneeded properties, which is rare in practice.
Propertized strings collapse this into "the candidate is the record." Each propertize call mints a fresh string object whose properties are bound to that exact instance.
What is not uniquely required. Some things look more constrained than they are.
category is not required by the framework. No VOMPECCC package reads our category text property; we discussed this above, and our filter helper and exporter are the only consumers. We could rename it to kind or class and only our own code would change. category was chosen for alignment with Emacs's vocabulary and the completion ecosystem's conventions, not for compatibility.multi-data bag is not required either. This corpus uses individual properties because the records are shallow and the property bag stays small, so every consumer asks one get-text-property question and gets one answer. In codebases like spot, where each candidate is a Spotify track or playlist with dozens of nested fields, the convention is to attach the full record under a single multi-data property and let consumers reach into the plist for deep fields. Both routes meet the substrate at the same hook.completing-read is concerned. You could put a UUID and have the annotator render the human name as a prefix. Most packages use the string as the visible name because it is simpler, not because they have to.The shape, in one sentence. A list of propertized strings is the shape that lets every VOMPECCC layer participate without your code translating between a "candidate" representation (what the framework sees) and a "record" representation (what your annotator, action, and transformer need). Every other shape forces that translation somewhere. Candidate-as-currency means: don't translate. Pass the same object end to end.
If you are reading this post in your own Emacs, your config may already have VOMPECCC packages enabled, which would muddy this demo of built-in completion. Run this block first to peel them off so the baseline really is the baseline.
;; Reset completion-styles and category configuration to Emacs defaults. (setq completion-styles '(basic partial-completion emacs22)) (setq completion-category-defaults nil) (setq completion-category-overrides nil) ;; Drop any custom Orderless wiring so dispatchers and matching styles don't leak in. (when (boundp 'orderless-style-dispatchers) (setq orderless-style-dispatchers nil)) (when (boundp 'orderless-component-separator) (setq orderless-component-separator " ")) (when (boundp 'orderless-matching-styles) (setq orderless-matching-styles '(orderless-literal orderless-regexp))) ;; Disable every VOMPECCC mode the post will switch back on layer by layer. (when (bound-and-true-p vertico-prescient-mode) (vertico-prescient-mode -1)) (when (bound-and-true-p prescient-persist-mode) (prescient-persist-mode -1)) (when (bound-and-true-p vertico-mode) (vertico-mode -1)) (when (bound-and-true-p marginalia-mode) (marginalia-mode -1))
After this block, completing-read should behave the way Emacs ships out of the box: a *Completions* buffer instead of a vertical list, prefix-only and partial-completion matching, no annotations, no contextual actions, and no 'frecency'. Phase 1 below demonstrates exactly that, and every subsequent phase adds one layer back.
completing-read) baseline
Before we pull in a single VOMPECCC package, it is worth seeing what the built-in Emacs completion already gives us. completing-read is a function in the Emacs standard library: it prompts the user for an input string, filters candidates based on that string, and then returns the chosen string. That is the entire contract.
(completing-read "Pick something: " my-produce)
When this block runs, the minibuffer opens with the prompt Pick something: = and a blinking cursor. Nothing else is visible at first. Press =TAB and a *Completions* buffer pops open above the minibuffer, laying all thirty produce names across columns. Type a prefix like pe and press TAB again; the *Completions* buffer shrinks to the two produce items whose names start with those letters (peach and pear). Once you pick an item (either by arrowing to it in *Completions* or typing its full name), you accept it with RET and the chosen string echoes into the message area.
This works, but it is visually and ergonomically primitive. Right now we are lacking display control for our candidate list, fuzzy matching, metadata display and filtering, preview, and any way of doing anything with the chosen item except receive its name.
Every phase that follows places layers of the VOMPECCC stack around this function without changing the function itself.
Every phase that follows places layers of the VOMPECCC stack around this function without changing the function itself.
Vertico only does one thing; it just gives us control over how candidates are displayed in the minibuffer. It does not filter, sort, annotate, or act on anything. Any code that calls completing-read; whether it's yours, Emacs's, a third-party package's; is rendered according Vertico's display settings if it is enabled.
(vertico-mode 1)
That is the entire integration! Re-evaluate the Phase 1 block:
(completing-read "Pick something: " my-produce)
This time the *Completions* buffer never appears. Instead, the minibuffer lays out candidates horizontally (my default display style for Vertico). The prompt still sits at the bottom, but to it's right is a flat array of all thirty produce items, one per line, with the first one (apple) highlighted as the current selection. C-n (or C-j in my config) walks the highlight to the right through pear, strawberry, blueberry, and so on through the list; C-p (or C-k in my config) walks it left. RET accepts whichever candidate is currently highlighted. The candidate list filters incrementally whenever we type a letter: typing a single p on the empty prompt collapses it to the 12 items whose names contains p, and each additional character narrows further in real time, with the selection snapping to the first surviving candidate.
Notice our completing-read function call did not change, and, critically, we did not pass Vertico the candidates. Vertico hooked into the minibuffer-setup pipeline at a lower level, and Emacs routes candidates to it. This is critical because this means Vertico now gives us candidate display control everywhere completing-read is used, without us havign to anything except enable vertico mode!
Vertico gave us a better view, but the matching is still the default combination of basic, partial-completion, and emacs22. These styles handle prefix and hyphen-segmented matches, but we lack a way to type more than one independent fragment, do any flex matching, negation, or any way to filter against candidate metadata.
Orderless is a completion style: it plugs into the completion-styles variable and changes how the input string is matched against candidates during completing-read. Orderless in practice reveals its namesake: it lets us split the input on a separator character, and return candidates that contain every component, in any order.
(setq orderless-component-separator ",") (setq orderless-matching-styles '(orderless-regexp) completion-styles '(orderless basic) completion-category-defaults nil completion-category-overrides '((file (styles basic partial-completion)))) ;; A small `tab' shim style: TAB completion only, no fall-through. ;; In-buffer completion (Corfu, the in-buffer counterpart from Post 2) ;; uses it; minibuffer prompts ignore it and pass straight through to Orderless. (add-to-list 'completion-styles-alist '(tab completion-basic-try-completion ignore "Completion style which provides TAB completion only.")) (setq completion-styles '(tab orderless basic))
Note: The basic fallback matters because a handful of Emacs features (TRAMP host completion, for example) require prefix-style matching that Orderless does not handle; basic catches those.
A few notes on the configuration we set up:
orderless-matching-styles is the chain Orderless uses against any input component that no dispatcher has claimed, and setting it to orderless-regexp is my personal recommendation.completion-category-overrides) is an idiomatic exception so that ~/d/s expands to ~/Documents/source/.,) keeps spaces available as part of any component you might want to match.tab style at the head of completion-styles is a one-line concession to in-buffer completion, because Corfu uses it for plain TAB completion in code buffers, but know that the minibuffer prompts ignore it and fall through to Orderless.Re-run the produce prompt:
(completing-read "Pick something: " my-produce)
You see Orderless in action when you type more than one component separated by commas. Typing a,e narrows the list to every item containing both an a and an e somewhere, in either order (Order-less, remember? 😜). Each comma-separated component is an independent filter, and the intersection (in the set algebra sense) of their matches is what survives in the candidate list display.
Individual components can override the default matching style through Orderless's style dispatchers, which are single-character affixes that tell Orderless to treat that component in a special way. Out of the box, Orderless ships orderless-affix-dispatch as the default dispatcher, mapping ~ to flex, ! to negation, & to annotation matching, , to initialism, and a few others. Two of those defaults are awkward in our setup: , is already serving as our component separator, so it can't double as a dispatcher prefix; and we want the annotation prefix to support a flex variant, which the affix-alist can't express because each entry maps a single character to a single style. Both reasons motivate replacing the defaults with hand-rolled versions tuned to our preferred prefix vocabulary.
We'll use a customized dispatcher set for the rest of this post.
Each dispatcher is a function of three arguments (pattern, index, total) that inspects a component and, if its dispatcher character appears at either end, returns (STYLE . PATTERN-WITHOUT-AFFIX) as so Orderless knows which matching style to apply. Returning nil passes the component to the next dispatcher in the chain, or to the default style if none match. Accepting the character as either prefix or suffix mirrors the built-in orderless-affix-dispatch and means you don't have to remember which end the trigger goes on, or preempt the matching style before you type out a component. ~bna and bna~ both flex-match.
;; Each dispatcher accepts the dispatcher character as either a ;; PREFIX or a SUFFIX of the component, mirroring the built-in ;; `orderless-affix-dispatch'. (defun my/orderless-dispatcher-initialism (pattern _index _total) "Initialism-match a component starting OR ending with a backtick." (cond ((string-prefix-p "`" pattern) `(orderless-initialism . ,(substring pattern 1))) ((string-suffix-p "`" pattern) `(orderless-initialism . ,(substring pattern 0 -1))))) (defun flex-if-twiddle (pattern _index _total) "Flex-match a component starting OR ending with `~'." (cond ((string-prefix-p "~" pattern) `(orderless-flex . ,(substring pattern 1))) ((string-suffix-p "~" pattern) `(orderless-flex . ,(substring pattern 0 -1))))) (defun annotation-if-at (pattern _index _total) "Annotation-match `@P' or `P@'. Flex-annotation-match `@~P', `~P@', `@P~', or `P~@' --- the inner `~' may sit at either end of the inner pattern." (let ((rest (cond ((string-prefix-p "@" pattern) (substring pattern 1)) ((string-suffix-p "@" pattern) (substring pattern 0 -1))))) (when rest (cond ((string-prefix-p "~" rest) (let ((re (mapconcat (lambda (c) (regexp-quote (char-to-string c))) (string-to-list (substring rest 1)) ".*"))) `(orderless-annotation . ,re))) ((string-suffix-p "~" rest) (let ((re (mapconcat (lambda (c) (regexp-quote (char-to-string c))) (string-to-list (substring rest 0 -1)) ".*"))) `(orderless-annotation . ,re))) (t `(orderless-annotation . ,rest)))))) (defun without-if-bang (pattern _index _total) "Exclude a literal match for a component starting OR ending with `!'. A bare `!' is a no-op (matches every candidate)." (cond ((equal "!" pattern) '(orderless-literal . "")) ((string-prefix-p "!" pattern) `(orderless-without-literal . ,(substring pattern 1))) ((string-suffix-p "!" pattern) `(orderless-without-literal . ,(substring pattern 0 -1))))) ;; `annotation-if-at' precedes `flex-if-twiddle' on purpose: ;; with suffix support, a compound like `@PATTERN~' would otherwise ;; fire flex on the trailing `~' before annotation could claim the ;; leading `@'. Annotation must get first crack. (setq orderless-style-dispatchers '(my/orderless-dispatcher-initialism annotation-if-at flex-if-twiddle without-if-bang))
Four dispatchers, each demonstrated against the candidate list:
Try the prompt again with these style dispatchers in place.
(completing-read "Pick something: " my-produce)
The annotation dispatcher is the most interesting of the four because it shows that some dispatchers compose. The leading @ picks a slot (annotation rather than name); the inner ~ switches the match style on whatever it's already pointing at. Nothing in Orderless's library knows about @~ as a compound prefix. We wrote that composition ourselves, in a few lines (annotation-if-at).
Note we won't be able to see the annotation dispatcher in action until we add annotations in the Marginalia section, so bear with me!
Orderless stays out of the way. The only thing that changed about our completion setup by introducing Orderless is how Emacs matches your input against the candidate list changed, because we effectively swapped in a different completion style through completion-styles.
Vertico didn't need to know about Orderless. Orderless didn't need to know about Vertico. They compose because Emacs routes their concerns through separate hooks.
To drive the point home, what we did not do matters architecturally: Vertico and Orderless are agnostic to one another. They leverage independent Emacs built-ins (completing-read rendering and completion-styles respectively), and they compose because Emacs natively routes their concerns through these separate channels. You can swap in any other minibuffer candidate display UI (Icomplete-vertical, Mct, fido-vertical-mode), or drop it entirely, and Orderless will still work. You can swap in any other completion style and Vertico will still work.
We can filter tightly now, but in spite of the rich metadata attached to each produce candidate, we can't see anything about a candidate except its name. If you're deciding between peach and apricot, relevant information like price, color, season, etc… is already on the candidate's text properties, but Vertico is only showing us the string itself. Marginalia is the package that promotes candidates into informed choices by displaying their metadata as right-aligned columns next to each candidate name.
The trick that makes Marginalia (and every subsequent VOMPECCC package) possible is the convention already established by the list of produce items: the candidate is its name as a string, with its full record's fields stamped on as text properties. completing-read is satisfied because the candidate is a plain string; the rest of the substrate is satisfied because the properties are right there.
A note on scope before we start: plain completing-read can only carry one completion category at a time, and Marginalia dispatches its annotator off that prompt-level category. The corpus has two categories (fruit and vegetable); to keep this phase honest, we narrow the picker to just fruits for now. Phase 5 will lift this restriction with Consult's multi-source mechanism.
That narrowing is one line of Lisp, but worth pausing on. Every later phase (the Consult sources in Phase 5, the async variant in Phase 8) will partition the corpus by category in the same way, so we factor it out once and read the routing key off the candidate itself rather than off of some separate lookup table. This is the candidate-as-currency convention in miniature: a routing question is answered by reading a text property:
(defun my-produce-of-category (cat) "Return candidates from `my-produce' whose `category' is CAT." (cl-remove-if-not (lambda (cand) (eq (get-text-property 0 'category cand) cat)) my-produce))
We still need to tell the prompt itself what completion category this is, because Marginalia dispatches its annotator off the prompt's metadata, not off per-candidate text properties. The candidate stamping we did in the corpus is for our own code (the actions, the transformer, the exporter) to read; the framework looks at prompt metadata. The lightest way to set the metadata is completion-extra-properties, a property list that overrides the completion metadata for the duration of one completing-read call:
(defun my-pick-fruit () "Pick a fruit by name." (interactive) (let ((completion-extra-properties '(:category fruit))) (message "You picked: %s" (completing-read "Fruit: " (my-produce-of-category 'fruit)))))
The foundational pattern — a completion function that responds to (action 'metadata) with (metadata (category . fruit)) — is still available and is what libraries like Consult build on. However, for a one-off picker without Consult, completion-extra-properties is equivalent and cleaner. Why is this even needed? Because plain completing-read over a list of strings has no way to communicate a category to Marginalia: the framework reads the prompt's completion metadata, and our list of strings doesn't ship any. As soon as we move to Consult in Phase 5, the source-level :category key takes over and the extra-properties step disappears entirely, which is exactly why packages like spot never need this dance. Every spot prompt is a Consult prompt. Our Phase 4 picker is the awkward case precisely because it is deliberately the barest possible call, and a demonstration of a Consult-less completion setup for our produce picker.
The annotator itself is the heart of this phase. It takes a candidate string, reads its text properties directly, and hands a list of columns to marginalia--fields, which does the alignment, per-field truncation, and face application:
(defun my-annotate-produce (cand) "Annotate a produce candidate CAND with type, color, season, and price." (marginalia--fields ((symbol-name (get-text-property 0 'type cand)) :truncate 13 :face 'marginalia-type) ((get-text-property 0 'color cand) :truncate 10 :face 'marginalia-string) ((get-text-property 0 'season cand) :truncate 12 :face 'marginalia-date) ((format "$%.2f" (get-text-property 0 'price cand)) :truncate 8 :face 'marginalia-number)))
Note that I am not writing any layout code at all. marginalia--fields handles padding, alignment, and face application; my job is only to declare which fields go in which columns. Annotating the candidates in this way enables Orderless's @ dispatcher to filter by our produce's metadata, so @berry, @citrus, @root become legitimate filter prefixes.
Registration for the fruit category is one add-to-list against Marginalia's annotator registry, plus enabling the marginalia-mode:
(add-to-list 'marginalia-annotators '(fruit my-annotate-produce none)) (marginalia-mode 1)
The registry entry is (CATEGORY ANNOTATOR1 ANNOTATOR2 ... none), where each tail symbol is an annotator marginalia-cycle (M-A) can rotate to in-session. We list two states for our category: our custom annotator, then none (annotations off). This matches spot's convention and gives a clean toggle on M-A. Marginalia's own built-in entries also include a builtin symbol in the chain (which is a fallback that defers to whatever annotation-function the prompt's metadata declares natively) but for a category nobody else knows about (like fruit), there is no built-in to defer to, so leaving it out keeps the cycle to two visibly-different states instead of three.
Run the picker:
(my-pick-fruit)
When (my-pick-fruit) runs, the prompt opens as it did before, but every one of the fruit candidates is now followed by a horizontally aligned set of columns: a type word (pome, berry, citrus, …), a color word (red, yellow, blue, green, …), a season (spring, summer, winter, year-round), and a dollar-formatted price ($0.59, $3.99, $6.99).
Stylistically, each column is padded to a fixed width and rendered in its own face, so the four fields read as distinct groups rather than running together with a delimiter. Where Phase 3 showed strawberry, this phase shows:
strawberry berry red spring $3.99
The list is legible at a glance, and what's more, you can usefully compare peach against apricot on price and season without typing anything. Scanning for cheap in-season fruit, for example, is made easy in this way.
Typing against annotation text is where Marginalia crosses from cosmetic to compositional. Typing yellow at the prompt matches nothing, because yellow is in the annotation column, not the candidate name, and Orderless is still matching against names only. But prefix that same component with @, as in @yellow, and the annotation dispatcher we wired up in Phase 3 tells Orderless to match this particular component against the annotation text instead of the candidate string. The list snaps to exactly the three yellow-colored fruits in the corpus.
To drive home the point that the VOMPECCC packages work independently of one another, Orderless knew nothing about Marginalia, and vice-versa. The @ dispatcher simply matches against whatever is in the "annotation slot" of the current candidate, and that slot happens to contain the words Marginalia stamped there.
The compound variant from Phase 3 cashes in here too: @~sm triggers the flex branch of annotation-if-at, flex-matching the characters s and m against annotation text. Only summer contains them in order, so the list collapses to the summer fruits.
Annotation components compose the same way regular components do. Typing @summer,@red on the empty prompt narrows first to summer fruits, then to the subset of those that are also red. The list collapses to raspberry and cherry, the two red summer fruits in the corpus. You can reach for a fruit by its properties without ever remembering its name. Post 2 called this "an unusually large leverage gain for what feels like a cosmetic layer".
The architectural observation here is that the @ dispatcher is not a Marginalia feature. It is an Orderless feature (a dispatcher we wrote) that happens to work because Marginalia exposes annotations through the same completion-metadata slot Orderless reads from. Swap Marginalia for any other annotator (say, a leaner one you write yourself that only shows price) and the @ filter still works. With an alternative annotation provider, Orderless would just filter against whatever that other annotator produces.
The Marginalia readme is explicit on a point worth surfacing before we lock this pattern in. For completion commands you control, the author recommends putting the annotator directly in the completion metadata via affixation-function, not in marginalia-annotators. Marginalia exists primarily to annotate other people's commands, and most of the time this is the built-in Emacs prompts and third-party packages whose authors didn't ship annotations themselves. When you control the call site, as we are here with our fruit picker, you can attach the annotator alongside the category in completion-extra-properties, and that is the recommended default.
The replacement is one function and one extra property. affixation-function receives the list of currently visible candidates and returns (CANDIDATE PREFIX SUFFIX) triples, which Vertico (or any completing-read UI) renders. We have to do our own column padding now: marginalia--fields was the convenience that absorbed it, but the upside of doing things the recommended way is that we eliminate the Marginalia dependency for this prompt:
(defun my-affixation-produce (cands) "Return (CAND PREFIX SUFFIX) per candidate, columns padded for alignment." (let ((width (apply #'max 0 (mapcar #'length cands)))) (mapcar (lambda (cand) (let* ((pad (make-string (- (1+ width) (length cand)) ?\s)) (cols (concat (propertize (format "%-13s" (symbol-name (get-text-property 0 'type cand))) 'face 'completions-annotations) (propertize (format "%-10s" (get-text-property 0 'color cand)) 'face 'completions-annotations) (propertize (format "%-12s" (get-text-property 0 'season cand)) 'face 'completions-annotations) (propertize (format "$%-7.2f" (get-text-property 0 'price cand)) 'face 'completions-annotations)))) (list cand "" (concat pad cols)))) cands))) (defun my-pick-fruit-builtin () "Pick a fruit using only built-in annotation machinery." (interactive) (let ((completion-extra-properties (list :category 'fruit :affixation-function #'my-affixation-produce))) (message "You picked: %s" (completing-read "Fruit: " (my-produce-of-category 'fruit)))))
The shape is the same as the Marginalia version (propertized candidates and a property list bound around the call) but with one extra entry. :affixation-function delivers the annotator directly, where the Marginalia version had only :category and let the marginalia-annotators registry lookup pick the annotator.
To prove this is independent of Marginalia, turn the mode off and run the new picker:
(marginalia-mode -1) (my-pick-fruit-builtin)
The four columns still appear. Vertico renders, Orderless filters, and the annotation slot is filled by my-affixation-produce. @yellow still works, too, because Orderless's annotation dispatcher reads from whatever populates that slot!
This actually strengthens the unix-style nature of the VOMPECCC set rather than diluting it. affixation-function in the completion metadata is an Emacs primitive. Marginalia is one consumer of that slot: a registry that picks an annotator per category and writes it into the slot at completing-read time. Our hand-rolled affixation-function is a different consumer of the same slot, delivering the annotator inline.
Re-enable Marginalia and re-run the original picker:
(marginalia-mode 1) (my-pick-fruit)
Annotations are back, served by Marginalia's registry-driven version. The two implementations are interchangeable from the prompt's point of view, and only the provenance of the annotator differs. When to reach for which becomes a question of who controls the command. For built-in Emacs prompts and third-party commands you can't edit, Marginalia is your only entry point. For your own commands, the inline affixation-function is closer to the data, has zero dependency, and is the route the Marginalia author recommends.
The rest of this post continues with Marginalia in place. Both routes (the Marginalia registry and the inline affixation-function) can be threaded through the Consult sources in Phase 5. The takeaway from this aside is that the inline route exists, that it is the recommended default for a completion command you fully control, and that it composes with Vertico, Orderless, and the rest of the completion substrate exactly the way the other VOMPECCC packages do.
That being said, I will use the Marginalia registry approach for the rest of this post because you will most often see Marginalia style annotations if you adopt VOMPECCC, and they look nicer with Marginalia's formatting.
Phase 4 was a single prompt over a fruit-only subset, which was enough to demo Marginalia, but it left the corpus's vegetables out and gave us no way to scope the prompt without typing. What if we want one prompt that holds everything (all categories) in my-produce and lets us flip between fruits and vegetables (and back to all of it) with a single keystroke.
Consult fixes this through its multi-source mechanism: consult--multi takes a list of sources, unifies their candidates into a single prompt, and sets up per-source narrowing keys. Each source declares its own name, its own narrow key, its own completion category, and its own items.
The dispatch story in Phase 5 is the same as in Phase 4: the candidate's completion category is whatever the candidate carries. In Phase 4 we read category off the only candidate type the picker held (a fruit). In Phase 5 each source produces candidates of one category (fruit for the fruit source, vegetable for the vegetable source) and the source-level :category matches the value the data already declared on every candidate. The framework reads the value out of the candidate. The spot post uses the same double-stamping pattern, and the per-candidate text property is the authoritative truth, and the source-level :category keeps Consult's metadata in sync.
Each candidate carries five text properties: category (fruit or vegetable), type, color, season, and price, and is read directly off the candidate by Marginalia, Embark, the annotator, and every action we're about to write.
Here we define two Consult sources (1 per category) each with its own scoped :history variable so previously-selected fruits and previously-selected vegetables don't get mixed together in any one history list:
(defvar my-consult-history-fruit nil "History scoped to the fruit Consult source.") (defvar my-consult-history-vegetable nil "History scoped to the vegetable Consult source.") (defvar my-consult-source-fruit `(:name "Fruits" :narrow ?f :category fruit :history my-consult-history-fruit :items ,(lambda () (my-produce-of-category 'fruit))) "Consult source for fruits.") (defvar my-consult-source-vegetable `(:name "Vegetables" :narrow ?v :category vegetable :history my-consult-history-vegetable :items ,(lambda () (my-produce-of-category 'vegetable))) "Consult source for vegetables.")
A Consult source plist's most important keys:
:items is the candidate stream. A function that returns a list of candidates, called lazily when the prompt opens. Ours is synchronous because the produce list is local, but for a remote API you would swap :items for :async and consult--dynamic-collection instead. Phase 8 walks through that variant against a faithfully-mocked produce API, and the spot post discusses the consumer-side cost when multiple sources share one endpoint.:narrow ?f binds the character that scopes the prompt to just this source. Pressing f SPC at the prompt hides every non-fruit candidate.:category fruit is the completion category that propagates onto every candidate from this source, not via our category text property but via Consult's own multi-category text property, which Consult writes from this very key. Marginalia and Embark consult that multi-category channel to pick the right annotator and the right keymap per candidate. We set this key to match the data's category property by hand, so the two declarations stay aligned.:history my-consult-history-fruit is a per-source history list. Selecting a candidate from this source appends it to this variable (and only this variable), which means the fruit picker can keep its own history and Vertico's M-p / M-n cycle through a list that is meaningfully scoped instead of polluted with every minibuffer interaction in the session. This per-source scoping is also what makes a per-prompt history-recall command (something like consult-history) useful, which we discuss right after.:name is the header shown above this source's candidates in the unified list.Two Marginalia registry entries, one per category:
(dolist (cat '(fruit vegetable)) (add-to-list 'marginalia-annotators `(,cat my-annotate-produce none)))
Then the consult command:
(defun consult-produce-picker () "Pick produce with multi-source Consult completion." (interactive) (consult--multi (list my-consult-source-fruit my-consult-source-vegetable) :prompt "Produce: "))
Run it:
(consult-produce-picker)
When (consult-produce-picker) runs, the prompt opens with the thirty produce items grouped under two bold header lines in the vertical list: Fruits and Vegetables, each followed by the candidates belonging to that source.
Every annotation column from Phase 4 is preserved because the annotator is registered against both categories and Consult has tagged each candidate with the right one.
The new affordance by consult is "narrowing", the ability to scope the prompt to a single source with one keystroke. Pressing f followed by SPC hides every non-fruit candidate: the twenty fruits become the entire list, and a small indicator in the prompt header shows the narrow state. Pressing DEL clears the narrow and restores the full thirty-item view. v SPC narrows to vegetables.
The features from earlier phases still apply inside a narrowed view, and this is where the independence of VOMPECCC's constituents really shows. With the list narrowed to fruits, typing @berry further filters to the four berries via the type column we added to the annotator; @red filters to red fruits; ~bry flex-matches the berry suffix of the remaining candidates; !blue excludes anything containing blue. Orderless, Vertico, and Marginalia are all still doing exactly what they did in Phase 4, and they could care less that the candidate source is now two Consult sources instead of one list.
Note we have another classifying property in each candidate. The type column in the annotator means @berry, @citrus, @root, @cruciferous are all valid one-component filters on the prompt, and they compose with the source narrow, so f SPC @citrus gives you exactly the four citrus fruits. The framework keeps type as a searchable axis instead of a narrowing axis.
The amount of code we wrote for the narrowing, grouping, and per-source history is pretty close to zero. I like Consult's declarative API in this way: we declared; Consult did.
consult-history
Now that fruit selections land in my-consult-history-fruit and vegetable selections land in my-consult-history-vegetable, two further capabilities become available almost for free.
The first is the built-in cycle: M-p and M-n walk through the active prompt's history, and because we scoped per source, those cycles only contain entries you actually picked from this source.
The second is more powerful: consult-history, a Consult command that reads the active history variable, presents its entries in a recursive minibuffer with full Orderless+Vertico+Marginalia goodness, and inserts the chosen entry into the original prompt. The conventional binding is in minibuffer-local-map:
(setq enable-recursive-minibuffers t) (keymap-set minibuffer-local-map "C-r" #'consult-history)
The enable-recursive-minibuffers setting is a prerequisite: consult-history opens a new minibuffer prompt while another is already active, and Emacs's default is to refuse that. Most VOMPECCC configurations turn it on already.
Demo 1: recall a previous selection. Run (consult-produce-picker) and pick cherry. Run it again, and at the empty prompt press C-r. A second minibuffer opens listing the active source's history; cherry is at the top. Pick it (or flex-match ~chy) and cherry is inserted into the original prompt — RET confirms it.
This is the everyday case: the items you reach for repeatedly are one keystroke and one history-pick away on every subsequent run. Since the history is per-source, narrowing first with f SPC or v SPC scopes C-r to just that source's history.
Demo 2: recall a complex Orderless query. Selections in history are useful, but the bigger win is recalling queries, for example, something like the complex multi-component Orderless filters you spent twenty seconds composing.
This is a behavior of consult history that is not so obvious. The trick is that, by default, completing-read adds whatever it returns to the history variable, and what it returns is the selected candidate. To make the typed query the return value instead, press M-RET (vertico-exit-input) at the prompt. This commits the literal minibuffer contents rather than the highlighted candidate, regardless of whether :require-match is on.
Run the picker, narrow with f SPC, and type summer,!blue,@red. Instead of pressing RET to pick a fruit, press M-RET. The minibuffer closes and the returned string is the literal summer,!blue,@red, and that is what lands in my-consult-history-fruit.
Run the picker again, narrow with f SPC, and hit C-r. summer,!blue,@red is at the top of history. Pick it, and the query is restored in the prompt, ready to be tweaked or RET-confirmed against the same filtered set.
Demo 3: save typed input on every exit. If you'd rather not have to remember M-RET, and you're willing to accept some history pollution, a minibuffer-exit-hook can capture the typed input alongside the selection on every exit:
(defun my-save-typed-input-to-history () "Add minibuffer input to history before exit, alongside the selection." (when-let* ((hist minibuffer-history-variable) ((symbolp hist)) ((not (eq hist t))) (input (minibuffer-contents-no-properties)) ((not (string-empty-p input)))) (add-to-history hist input))) (add-hook 'minibuffer-exit-hook #'my-save-typed-input-to-history)
With the hook in place, every exit pushes the current contents of the minibuffer to history. RET on raspberry after typing summer,!blue,@red now adds both summer,!blue,@red and raspberry to history. C-r shows both. The cost is that history gets longer, including incomplete queries you abandoned mid-typing. But consider you also have the fully feature ICR experience in consult-history, so in my opinion, this is a non-issue.
Indeed, architecturally, history is one more consumer of the same Emacs primitive every other phase has been consuming. completing-read takes a HIST argument and updates that variable on selection, and Consult's per-source :history key just threads that argument per source. consult-history reads the variable. The minibuffer-exit-hook reads the buffer's typed contents. M-RET changes what gets returned from the prompt and therefore what gets stored.
We can select a produce item, but we still can't do anything with it.
So far the picker's job has ended at returning a string. Embark is the package that adds a layer of category-contextual actions on top of any completion prompt and provides a keyboard-driven context menu whose contents depend on what kind of candidate is highlighted.
It's useful to think of Embark as similar to a left-click or 'context menu' in traditional UIs.
We'll define the minimum set of actions needed to demonstrate Embark's three headline features:
embark-act on a single candidate (the basic case): an inspect action that pops a buffer with the item's full plist.embark-act-all on every visible candidate (multi-cardinality): an add-to-cart action that appends to a *Cart* buffer, which we will invoke against every currently visible item at once.roast action that fruits don't; citrus fruits get a juice action that other fruits don't. These are specializations side by side, by two different mechanisms in Embark: the vegetable specialization rides on the Consult source's :category key (forwarded to Embark via Consult's multi-category channel), while the citrus specialization rides on an Embark transformer we write that reads the candidate's type property.Each action is a regular interactive command that takes the candidate string as its argument. Embark passes the current candidate when the user triggers the action.
(defun my-inspect-produce (cand) "Pop a buffer showing the text properties carried by produce CAND." (interactive "sProduce: ") (let ((props (text-properties-at 0 cand)) (buf (get-buffer-create "*Produce Inspect*"))) (with-current-buffer buf (erase-buffer) (insert (format ";; %s\n" cand)) (pp props (current-buffer)) (goto-char (point-min)) (emacs-lisp-mode)) (display-buffer buf))) (defun my-add-to-cart (cand) "Append produce CAND to the *Cart* buffer." (interactive "sProduce: ") (let ((price (get-text-property 0 'price cand))) (with-current-buffer (get-buffer-create "*Cart*") (goto-char (point-max)) (insert (format "- %-12s ($%.2f)\n" cand price))) (message "Added %s to cart" cand))) (defun my-make-juice (cand) "Juice the citrus fruit CAND." (interactive "sFruit: ") (message "🍹 Juicing %s!" cand)) (defun my-roast-vegetable (cand) "Roast the vegetable CAND." (interactive "sVegetable: ") (message "🔥 Roasting %s!" cand))
Each action reads whatever properties it needs off the candidate, and then does something domain-specific. No Embark machinery leaks into the action body, and Embark's only job is to invoke the action with the right candidate.
Embark actions are bound in per-category keymaps. We define one general map per top-level category (one for fruits, one for vegetables), and then we define a specialized citrus map that inherits from the fruit map and adds j (for juice 🍹):
(defvar-keymap my-fruit-general-map :parent embark-general-map :doc "Embark actions applicable to any fruit." "i" #'my-inspect-produce "a" #'my-add-to-cart) (defvar-keymap my-vegetable-general-map :parent embark-general-map :doc "Embark actions applicable to any vegetable." "i" #'my-inspect-produce "a" #'my-add-to-cart "r" #'my-roast-vegetable) (defvar-keymap my-fruit-citrus-map :parent my-fruit-general-map :doc "Embark actions for citrus (fruit actions + juicing)." "j" #'my-make-juice)
:parent embark-general-map gives us the built-in defaults (copy-as-kill, describe, insert, etc.) on top of our domain-specific actions. :parent my-fruit-general-map on the citrus map means citrus candidates inherit i and a from the fruit general map and add j on top.
fruit to fruit-citrusWe have two dispatch problems in this phase, and they look symmetric on the surface:
:category onto each candidate via the multi-category text property. We want Embark to pick my-fruit-general-map for fruits and my-vegetable-general-map for vegetables.j juice action via my-fruit-citrus-map. This is a refinement below the source category.
The corpus already keeps two clean axes for these: category for top-level kind (fruit or vegetable) and type for fine-grained classification (pome, berry, citrus, …). We don't want to declare citrus as a third completion category — that would collapse the two axes back into one, force the data's category field to mean both "fruit-vs-vegetable" and "this particular kind of fruit", and the next refinement (say, expensive citrus) would force yet another category symbol. The right tool is Embark's transformer mechanism: keep the framework's category vocabulary flat (just fruit and vegetable), and specialize below that vocabulary by reading additional fields off the candidate.
A transformer is a function attached to a category in embark-transformer-alist. When Embark is about to dispatch on a candidate, it looks up the prompt's category in that alist; if it finds a transformer, it calls the transformer once with the original type and target. The transformer returns a refined (TYPE . TARGET) cons, and Embark dispatches on that refined type.
Now the load-bearing detail: Embark applies the transformer exactly once. Look at embark.el's dispatch path and you'll see (if-let (transform (alist-get type embark-transformer-alist)) ...) — a single if-let, no recursion, no chain. The transformer for the original prompt type fires, returns a refined type, and Embark dispatches on that refined type without consulting the alist a second time.
This is consequential when Consult is in the picture. consult--multi sets the prompt's completion-metadata category to multi-category (not fruit or vegetable) and stamps each candidate with a multi-category text property holding (SOURCE-CATEGORY . CAND). Embark ships a built-in transformer registered on multi-category (embark--refine-multi-category) that extracts the source category from that property and returns (fruit . CAND) or (vegetable . CAND). But that's the only transformer Embark will run. If we naively wrote a fruit transformer and registered it on fruit, it would never fire in our prompt, because the prompt's category is multi-category and Embark only consults the alist once, against that original key.
So in a multi-category prompt, the multi-category transformer is the only integration point for any refinement below the source level. We replace Embark's built-in with our own that does both jobs in a single pass: the same source-category extraction (delegated to the built-in helper), plus our citrus refinement on top.
(defun my-multi-category-transformer (type cand) "Refine `multi-category' candidates with citrus refinement layered on. Embark applies the prompt-category transformer exactly once, so any per-source refinement must happen inside this single function. We delegate to Embark's built-in `embark--refine-multi-category' for the source-category extraction, then refine `fruit' to `fruit-citrus' when the candidate's `type' property is `citrus'." (let ((refined (embark--refine-multi-category type cand))) (if (and (eq (car refined) 'fruit) (eq (get-text-property 0 'type cand) 'citrus)) (cons 'fruit-citrus (cdr refined)) refined))) (setf (alist-get 'multi-category embark-transformer-alist) #'my-multi-category-transformer)
Then we register one keymap per target type. Every fruit gets the general fruit map, citrus gets the specialized one (via our transformer's refinement), and every vegetable gets the vegetable map:
(add-to-list 'embark-keymap-alist '(fruit . my-fruit-general-map)) (add-to-list 'embark-keymap-alist '(fruit-citrus . my-fruit-citrus-map)) (add-to-list 'embark-keymap-alist '(vegetable . my-vegetable-general-map))
What about embark-act-all over a candidate set that spans both sources? When all candidates share a refined type (after our transformer runs), Embark dispatches on that unified type — so a fruits-only narrowing dispatches on fruit, and a vegetables-only narrowing dispatches on vegetable. When the set spans sources (e.g., a cross-cutting @summer filter), Embark falls back to the original prompt category, multi-category. We register a fallback keymap so cross-source embark-act-all still has i and a available:
(add-to-list 'embark-keymap-alist '(multi-category . my-fruit-general-map))
This is the integration, end to end. consult--multi sets the prompt category to multi-category and stamps each candidate with a multi-category text property holding the source category. Embark looks up multi-category in embark-transformer-alist and runs our transformer, which extracts the source category (via the built-in helper), then refines fruit to fruit-citrus when the candidate's type property is citrus. Embark dispatches on the refined type, finds the matching keymap in embark-keymap-alist, and opens it. One transformer, three keymap entries plus a fallback — all riding on the per-source :category we declared in Phase 5 and the type field we put on every candidate at the top of the post.
Assuming you have embark-act bound somewhere (the canonical binding is C-.; if you don't, evaluate (keymap-global-set "C-." #'embark-act) first):
(consult-produce-picker)
Let's start by acting on a single candidate. Run the picker, narrow or scroll until apple is highlighted, and press C-.. A small keymap-hint popup appears, listing the action keys available for this candidate: i for inspect, a for add-to-cart, plus everything embark-general-map inherits. Pressing i closes the popup, closes the minibuffer, and pops open a new buffer called *Produce Inspect* showing the apple's text properties pretty-printed: a comment line ;; apple followed by (category fruit type pome color "red" season "fall" price 1.29). Embark invoked my-inspect-produce with the candidate string; our action grabbed the candidate's text-property plist via text-properties-at and handed it to pp.
Now clear the prompt and scroll to lemon. Press C-.. The menu is different this time! i and a are still there, but there is a third entry: j juice. Inside our multi-category transformer, the source-category extraction returns fruit, and then the citrus check sees (get-text-property 0 'type cand) = citrus and refines the dispatch type to fruit-citrus. Embark looks up fruit-citrus in the keymap alist, finds my-fruit-citrus-map (which inherits the i and a bindings from the general map and adds j on top), and presents it after embark-act. Press j and 🍹 Juicing lemon! flashes into the echo area.
Try embark-act on strawberry. The popup is back to just i and a, with no j in sight. A strawberry's type is berry, so the citrus branch of our transformer doesn't fire, the refined type stays fruit, and Embark picks my-fruit-general-map. Now try carrot: the popup is i a r. r roast is there because carrot came from the vegetable source — our transformer extracts vegetable from multi-category, the citrus branch is irrelevant (the source category is not fruit), and Embark dispatches on vegetable.
For the multi-candidate case, start the picker again and narrow to fruits with f SPC, then type @summer to filter to summer fruits within fruits. The list collapses to the summer fruits. Press embark-act-all (the canonical binding is A from the embark-act popup, or C-u C-. directly). Embark prompts for a single action to apply to every visible candidate; press a for add-to-cart. In one keystroke, all ten summer fruits are appended to the *Cart* buffer, one per line, each with its price. You can also select individual candidates with embark-select (which I have bound to C-SPC).
Now clear the narrow and try @summer across the full produce corpus. Hit embark-act-all, press a for add-to-cart. All thirteen go into *Cart*. This is where the multi-category fallback we registered earlier comes in handy, because the candidate set spans both sources, embark dispatches against the multi-category type, and the fallback gives us i and a (the actions defined on every produce item). Type-specific actions like j and r aren't available in this state, which makes logical sense, because a single keystroke can't simultaneously juice a citrus and roast a vegetable.
The same text properties that Marginalia was reading in Phase 4 are what the action functions and the transformer are reading now. Three different packages are cooperating, on three different hooks, off the same candidate, without ever knowing about each other.
So far the picker has been a selection surface: pick something and act on it.
Embark provides two further commands that promote the candidate list itself into a first-class buffer.
embark-collect snapshots the current candidate set into a fresh *Embark Collect* buffer with the original keymap dispatch still attached to each row. Run the picker, type @summer to narrow to summer items across both sources, then M-x embark-collect (or invoke embark-act with a prefix arg and pick embark-collect). The thirteen summer items land in a buffer that outlives the minibuffer; press embark-act on cherry and the fruit map dispatches; press embark-act on tomato and the vegetable map dispatches. No registration is needed; embark-collect works on any candidate set automatically.
embark-export goes one step further. Where collect produces a generic buffer of strings, export materializes the candidates into the major mode appropriate to their type. File candidates become a Dired buffer; buffer candidates become an Ibuffer; grep results become a wgrep-editable buffer; etc…. For our produce, the natural target is Emacs's built-in tabulated-list-mode.
We register a per-category exporter the same way we registered keymaps in The Keymaps. The exporter takes the list of candidate strings and produces the target buffer:
(defun my-export-produce-tabulated (candidates) "Export produce CANDIDATES into a tabulated-list buffer." (let ((buf (get-buffer-create "*Produce*"))) (with-current-buffer buf (tabulated-list-mode) (setq tabulated-list-format [("Name" 14 t) ("Cat" 10 t) ("Type" 13 t) ("Color" 10 t) ("Season" 12 t) ("Price" 8 t)]) (setq tabulated-list-entries (mapcar (lambda (cand) (list cand (vector (substring-no-properties cand) (symbol-name (get-text-property 0 'category cand)) (symbol-name (get-text-property 0 'type cand)) (get-text-property 0 'color cand) (get-text-property 0 'season cand) (format "$%.2f" (get-text-property 0 'price cand))))) candidates)) (tabulated-list-init-header) (tabulated-list-print)) (pop-to-buffer buf))) (dolist (cat '(fruit fruit-citrus vegetable multi-category)) (add-to-list 'embark-exporters-alist (cons cat #'my-export-produce-tabulated)))
The exporter reaches for the same text properties that the annotator and the actions read from. Once again, the candidate-as-currency convention is being leveraged.
The four registrations are worth a moment. embark-export unifies the candidate set's type by calling our transformer on every candidate and checking that they all produce the same refined type ((cl-every ...) in embark--maybe-transform-candidates). When all candidates are vegetables, our transformer returns vegetable for each, unification succeeds, and Embark dispatches on vegetable. Same for an all-citrus filter (f SPC @citrus → all fruit-citrus) or an all-non-citrus filter. But the obvious case (narrow to fruits with f SPC) gives a mix of citrus and non-citrus, which our transformer refines to two different types (fruit-citrus and fruit). Unification fails, and Embark falls back to the prompt's original category: multi-category. Registering our exporter on multi-category as well makes the export work in that mixed-but-still-produce case.
A trade-off worth being honest about: that multi-category registration is shared with every other consult--multi prompt in the session. In a real package, you'd want to guard the exporter against non-produce candidates, or scope the registration more tightly. For this walkthrough we accept the broad registration and move on.
Run the picker, narrow to fruits with f SPC, then M-x embark-export (or embark-act with prefix arg, then E). A *Produce* buffer pops open in tabulated-list mode:
(consult-produce-picker)
Name Cat Type Color Season Price apple fruit pome red fall $1.29 pear fruit pome green fall $1.79 strawberry fruit berry red spring $3.99 blueberry fruit berry blue summer $4.99 raspberry fruit berry red summer $5.99 ...
Every column is sortable from its header (the trailing t flag in tabulated-list-format turns sortability on per-column); n and p move between rows. Since each row's tabulated-list-id is the original propertized candidate, embark-act on a row still dispatches to the right keymap based on category, including the citrus juicer via the transformer. embark-act on the cherry row offers i a (general fruit); embark-act on lemon offers i a j (citrus, refined by the transformer); embark-act on a row exported from a vegetable narrowing would offer i a r (vegetable). Nothing changes about the per-candidate dispatch just because the candidates moved into a major mode, which is very convenient! This is one of my favourite things about Embark. It works on minibuffer candidates in the all-too-common ICR setting, but it also works on any arbitrary piece of text in Emacs. With Embark, literally everything in Emacs becomes left-clickable in this way.
Cross-cutting filters (e.g., @summer spanning fruits and vegetables) also fall back to multi-category for the same reason (the candidates' refined types differ) so the same registration carries them through. embark-collect remains available as a finer-grained alternative when you want a generic candidate-set buffer rather than a tabulated table.
The architectural take: embark-export is not a Consult feature, not a Marginalia feature, not part of any pipeline our other phases set up. It is an Embark feature that works with any completing-read-driven prompt that has a category and a registered exporter. And the exporter we wrote is one more consumer of the candidate's text properties alongside the annotator (Phase 4), the action functions (this phase), the transformer (this phase), and the per-category keymap dispatch (also this phase). If you ever decide a tabulated list isn't the right export target (maybe a csv-mode or org-mode would be better) swap the exporter function and every other layer of the picker stays exactly where it is.
The last concern from the orthogonal six is sorting.
So far, sorting has been incidental. Vertico ships a default sort that draws on minibuffer history, but it ignores frequency entirely and resets when Emacs restarts. Prescient is the dedicated package for this concern: it computes a combined frecency score (recency + frequency) per candidate and hands it to Vertico as the sort function. With prescient-persist-mode the score survives Emacs restarts; even without it, the score updates from the very first selection.
That last point is what makes Prescient demonstrable in a from-scratch walkthrough. The score for a never-selected candidate is zero. Pick a candidate once and its score jumps above zero, putting it at the top of the next prompt. One selection is enough to see the effect.
One wrinkle to acknowledge before the setup. Of the six VOMPECCC packages we are using, Prescient is the only one that bundles two concerns: sorting (the part we want here) and its own filtering style. vertico-prescient-mode toggles both by default — which would replace the Orderless setup from Phase 3 and silently break the custom dispatchers (~, !, backtick, @) we have been relying on for the last four phases. Post 2 flags this same two-concern split and notes the common workaround: keep Orderless for filtering, take Prescient's sort, and disable the filter takeover. That is what we do here:
(setq vertico-prescient-enable-filtering nil) (vertico-prescient-mode 1)
This is the entire prescient integration. (Adding (prescient-persist-mode 1) would persist scores across Emacs sessions; we leave it off here so the demo's state stays bounded to this single Emacs session.)
This demo is simple. Run the picker once, select an item, then re-run. The item we just picked will be the first in the list.
(consult-produce-picker)
All five layers continue untouched, while a sixth package quietly improves the order in which their candidates land on screen.
consult--dynamic-collection async consult remote
The picker we have works because the entire produce corpus is local: my-produce is a literal thirty-item list, and each Consult source's :items callback returns its slice synchronously. Real-world sources are usually remote, and the spot post demonstrates this w/r/t the Spotify API. Filtering at the Emacs side stops being viable once the corpus has thousands or millions of entries, and you have to push the query to the server and let the server tell you which candidates match.
Consult ships consult--dynamic-collection for exactly this case. It wraps a completion function so that each keystroke fires a debounced, cancellable query. The function you write only has to answer "given this query string, what are the candidates?" Consult handles the debouncing (default ~200ms via consult-async-input-debounce), the cancellation of stale responses (via while-no-input), and the display refresh.
We don't have a real produce service to hit, but we can mock one faithfully enough to demonstrate the pattern. Real search backends (Spotify, GitHub, Algolia, Slack, …) almost always do something fuzzier than literal substring matching, so the mock matches that style. Each character of the query must appear in the candidate's name in order, possibly with gaps in between, case-insensitively. The mock takes a query string, sleeps 😴 to simulate network round-trip, and returns the matches:
(defvar my-mock-api-latency 0.1 "Simulated API latency in seconds. Set to 0 to disable the sleep.") (defvar my-mock-api-call-count 0 "Counter so we can watch the mock API being hit.") (defun my-mock-fuzzy-match-p (query name) "Return non-nil if NAME flex-matches QUERY (chars in order, case-insensitive)." (let ((case-fold-search t) (re (mapconcat (lambda (c) (regexp-quote (char-to-string c))) (string-to-list query) ".*"))) (string-match-p re name))) (defun my-mock-produce-api (query) "Pretend to fetch produce from a remote API matching QUERY. Sleeps for `my-mock-api-latency' seconds to simulate network round-trip, then returns the subset of `my-produce' whose names fuzzy-match QUERY." (cl-incf my-mock-api-call-count) (when (> my-mock-api-latency 0) (sleep-for my-mock-api-latency)) (cl-remove-if-not (lambda (cand) (my-mock-fuzzy-match-p query cand)) my-produce))
A real client would issue an HTTP request, parse JSON, and propertize the response. Ours is a fuzzy regex match over the corpus, gated by sleep-for so latency is observable. The call counter lets us watch the API being hit (or not) as we type.
The completion function takes a query and returns a list of propertized candidates, the same text properties as the synchronous version (category, type, color, season, price all carried directly on each candidate). Only the source has changed. Because my-produce is already a list of propertized strings, the completion function reduces to a category filter on whatever the API returns. We close over cat so each completion category gets its own completion function:
(defun my-async-produce-candidates (cat) "Return a completion function that yields CAT-category produce matching QUERY." (lambda (query) (cl-remove-if-not (lambda (cand) (eq (get-text-property 0 'category cand) cat)) (my-mock-produce-api query))))
The Consult source uses :async rather than :items, wrapping the completion function with consult--dynamic-collection:
(defvar my-consult-history-fruit-async nil) (defvar my-consult-source-fruit-async `(:name "Fruits (async)" :narrow ?f :category fruit :async ,(consult--dynamic-collection (my-async-produce-candidates 'fruit) :min-input 1) :history my-consult-history-fruit-async) "Async Consult source for fruits.") (defun my-async-produce-picker () "Pick a fruit using an async Consult source." (interactive) (consult--multi (list my-consult-source-fruit-async) :prompt "Fruit: "))
Run it:
(my-async-produce-picker)
The prompt opens empty; :min-input 1 keeps Consult from firing until you type at least one character. Type b and you will see a moment of latency, and then five fruits whose names contain b (the four berries and banana). Type e more (be), and you will see another fetch, the four berries appear because each one has a b followed by an e somewhere in its name, while banana has no e and drops out.
Empty the prompt and type rapidly: papaya, holding no key for long. Consult's debouncer drops intermediate queries, and in a resource-sparing manner, only the queries you paused on reach the API. When a slow query is mid-flight and your input changes, while-no-input aborts the in-flight computation and restarts on the new input, so stale results never make it to the candidate list. The completion function we wrote is plain synchronous Lisp; all the cancellation, debouncing, and refresh logic are consult--dynamic-collection's problem, not ours.
A small architectural symmetry worth flagging. For Consult's "async command" family of commands (consult-grep, consult-ripgrep, consult-find, and friends) input is additionally split between a backend portion and a local-filter portion via consult-async-split-style. Setting it to 'comma aligns the split character with our orderless-component-separator, so the same , delimits "send this to the server" and "filter the result locally with Orderless." consult--dynamic-collection (the lighter wrapper we used here) skips the split and forwards the whole input to the completion function, but the harmonisation is right there for prompts that want both.
Step back and count what each package contributed:
| Package | What it gave us | Lines we wrote |
|---|---|---|
| Vertico | Display control of candidates | 1 |
| Orderless | Multi-component matching, flex, negation, annotation filter dispatch | ~3 |
| Marginalia | Four right-aligned metadata columns + annotation-aware matching | ~12 |
| Consult | Two sources, unified prompt, per-source narrow keys | ~20 |
| Embark | Per-category action keymaps + transformer for citrus refinement + tabular export | ~50 |
| Prescient | Frecency sort that promotes recently/frequently picked candidates | 2 |
Roughly 90 lines of VOMPECCC-facing emacs-lisp, and we have a categorised, narrowable, annotated, annotation-matchable, action-bindable, transformer-refined, exportable, frecency-sorted produce picker (that's a mouthful!). There is no UI code anywhere in it! Every line fell into one of two buckets: configuring a VOMPECCC package (add-to-list, setq, defvar-keymap) or declaring our data (the propertized corpus, the annotator, the action bodies, the transformer).
The how of the composition, the question Post 2 gestured at and Post 3 answered for a larger application, is a single convention repeated across the six packages. Each candidate is its name as a string, with its full record's fields stamped on as text properties:
(propertize "apple" 'category 'fruit ; our internal routing key (filter, exporter) 'type 'pome ; read by our Embark transformer 'color "red" ; annotation column 'season "fall" ; annotation column 'price 1.29) ; annotation column
That is the entire integration surface. The exporter and our filter helper read the candidate's category property directly; the annotator reads type, color, season, price; the Embark transformer reads type. Vertico, Orderless, and Prescient do their work on the string itself. Marginalia and Embark dispatch off the prompt's completion-metadata category, which Consult populates from each source's :category key (in turn set to match the data's category property by hand — the candidate-as-currency convention paying its dues by keeping our routing key and Consult's source key in lockstep). When Embark wants to dispatch below the granularity of category, e.g.\ with citrus-only juicing inside the broader fruit category, our transformer reads type off the candidate and refines accordingly. These six packages communicate interoperably through Emacs's completion substrate, with one shared currency: the propertized candidate.
A note on shape. We carry the produce fields as individual text properties because the records are shallow and the property bag stays small. Codebases like spot, where each candidate is a Spotify track or playlist with dozens of nested fields, take a different approach and attach the entire record under a single multi-data property and let consumers reach into the plist for deep fields. Both shapes meet the substrate at the same hook (a propertized candidate whose properties happen to encode a typed record) and the choice between them is purely local to the corpus.
Replace my-produce with GitHub issues, S3 buckets, Slack channels, ten thousand Org headings, a music library, or your ticketing system. The integration surface is unchanged. The corpus reshapes; the annotator reads different fields; the Consult sources partition differently; the Embark keymaps and transformer do different things. Critically, none of the framework code moves. This is the architecture behind spot, consult-gh, consult-notes, consult-omni, and the growing ecosystem of ICR-driven packages that can be assembled from the same substrate without rebuilding any of its layers.
A handful of VOMPECCC features are worth pointing at, because you will want them eventually and they are all continuations of the same substrate pattern you just saw:
:state key accepts a function that fires on candidate navigation, not just selection. consult-line uses it to scroll the current buffer to the highlighted line; consult-theme applies the theme as you scroll. For a produce picker, :state could pop a "tasting notes" buffer that updates as you move through the list.fruit → fruit-citrus based on type. Transformers can refine on any predicate over the candidate's properties — e.g., turn every produce item into produce-expensive if price > 3.00, so you can layer arbitrary fine-grained dispatch on top of the coarse category without inventing new completion categories. There is a huge amount of power there, especially when you understand that Embark's utility expands greatly when you are willing to start typing entities in Emacs, whether they are in the minibuffer or regular buffers.prescient-persist-mode. Phase 7 enabled vertico-prescient-mode without persistence to keep the demo bounded; in a real configuration, (prescient-persist-mode 1) writes the frecency table to disk so your most-picked candidates stay at the top across Emacs restarts.Each of these is an incremental add on the same foundational Emacs completion substrate.
This post built a 30-item produce picker in Emacs by layering six VOMPECCC packages one at a time on top of stock completing-read. Vertico rendered the candidate list vertically with a one-line mode activation; Orderless added multi-component matching via the completion-styles hook plus four custom style dispatchers (~ flex, backtick initialism, ! negation, @ annotation with a @~ flex variant), each accepting its trigger character at either prefix or suffix position; Marginalia added four metadata columns per candidate (type, color, season, price) by reading text properties attached to each candidate string with propertize, and along the way made the annotation text itself searchable through Orderless's @ dispatcher — including the type column, so @berry and @citrus become valid one-component filters; Consult's consult--multi unified the corpus into two categorised sources (fruit, vegetable) under one prompt with per-source narrow keys and per-source :history variables, with each source's :category matching the value the data declares, and consult-history bound to C-r turning those scoped histories into a recall surface for both prior selections and (via M-RET or a minibuffer-exit-hook) prior Orderless queries; Embark attached contextual actions via per-category keymaps (one for fruits, one for vegetables, one specialized for citrus) plus an Embark transformer registered on multi-category that does both jobs in a single pass — because Embark fires the prompt-category transformer exactly once, the multi-category transformer is the one place to put both source-category extraction (delegating to the built-in helper) and our citrus refinement (reading type off the candidate and refining fruit to fruit-citrus) — and an embark-export path that materialises candidates into a sortable tabulated-list-mode buffer; Prescient then hooked Vertico's sort slot to surface recently and frequently picked candidates first, with the rank visibly shifting after a single selection; finally an async variant swapped :items for consult--dynamic-collection over a faithfully-mocked produce API, demonstrating debounced/cancellable remote queries and discussing when the spot-style cache+mutex pattern is load-bearing (multiple sources sharing one endpoint) versus defensive ceremony (single thread, single source). The entire integration surface across the six packages was one propertized candidate carrying its fields as text properties — category as our internal routing key, kept aligned with each Consult source's :category so the framework's own multi-category channel matches; type read by our Embark transformer; the rest for annotation columns — no package calls another's API, none imports another's internals, and swapping my-produce for any other typed corpus would leave every line of the framework configuration unchanged.
-1:-- VOMPECCC from Scratch: Picking Produce with ICR in Emacs (Post Charlie Holland)--L0--C0--2026-04-28T09:14:00.000Z
After adding the email comment invite facility to
BlogMore it only made sense that I add
some commands to blogmore.el to
make it easier to edit the front matter that can help drive that feature.
So... I've released v4.3.0 of blogmore.el that adds two new commands:
blogmore-toggle-invite-comments -- toggles the comment invitation
propertyblogmore-invite-comments-to -- makes it easy to set, edit or remove the
email address to use when making the inviteI've also added the two commands to the transient menu, using C-t for the former and C-a for the latter.
-1:-- blogmore.el v4.3.0 (Post Dave Pearson)--L0--C0--2026-04-28T07:30:43.000Z
MarkEdit
Product: MarkEdit
Price: Free
If you read my blog you know that I am an Emacs user. I write Markdown in Emacs. I write everything in Emacs. But I have been curious about MarkEdit for a while and decided to check it out.
This is a free and open-source Markdown based text editor. The developer describes it thus “It’s just like TextEdit on Mac but dedicated to Markdown.”
This is what the developer says about what makes MarkEdit different:
- Privacy-focused: doesn't collect any user data
- Native: clean and intuitive, feels right at home on Mac
- Fast: edits 10 MB files easily
- Lightweight: installer size is about 4 MB
- Extensible: seamless integration with Shortcuts and AppleScript
MarkEdit delivers on its promises. It is the second best Markdown based text editor in the marketplace. Of course, the best is iA Writer. By virtue of being free, MarkEdit might be better value than any of the competition, including iA Writer.
I am impressed with MarkEdit. It does everything you expect from a product in this category and does it well.
Unlike iA Writer, MarkEdit gives you options. It has a set of preferences designed to make it the editor you want to use.
pref1
MarkEdit lets you choose your own fonts. iA Writer doesn’t.
MarkEdit has themes. iA Writer has only light and dark options.
pref2
MarkEdit has completions you can customize them. iA Writer does completions but you cannot customize them. They are the usual system completions.
pref3
You can set your own preferences for the extension for your Markdown files. You can do that in iA Writer by typing it at the point of saving the file. It is not an application wide preference.
On the whole, MarkEdit’s preferences are well-designed and well-implemented with all the things you need to make it conform to your idea of an ideal Markdown based text editor. iA Writer has a different approach. It makes the choices for you with the assumption that you are better off concentrating on the words that you are going to write in the program. They take care of how the program needs to work. Both of these approaches work, and both programs are fantastic editors.
keyboard commands
I hate going to the mouse when I am writing. MarkEdit saves me from that with its support for keyboard commands. They are extensive and make MarkEdit an absolute pleasure to use.
MarkEdit has extensions and user-designed themes. They are available here: Extensions · MarkEdit-app/MarkEdit Wiki.
MarkEdit implements an API, so developers can write extensions to extend the features of MarkEdit. It even has an extension to implement Vim keybindings.
I like the Markdown Table Editor extension.
The difference of the philosophy behind the development of iA Writer and MarkEdit is clear from the presence of the API and its effect.
iA Writer does a lot more than MarkEdit when it comes to things like managing your file-system. MarkEdit makes no attempt to do that. It is a single-file application. Or more correctly, thanks to the tab implementation, it is a file based application. It doesn’t know anything about folders and the management of that.
I love iA Writer. I have been using it for a long time. Since version 1 of the program actually. It is a charming little editor which I enjoy using. MarkEdit surprised me. It is a well-designed text editor which handles all of the Markdown tasks you can imagine. And it is free. It is extendable, customizable, and efficient for writing in Markdown.
My only gripe? Full-screen mode in MarkEdit sucks. The document cannot cover the whole screen. No one should be made to write like that. The document window should be a window in the middle with the line lengths the same as the regular window, in full-screen mode. For a program which is so well designed, this omission was a surprise.
To make up, MarkEdit comes with a good manual at Manual · MarkEdit-app/MarkEdit Wiki.
If you are looking for a great text editor which handles Markdown you cannot go wrong by choosing MarkEdit.
macosxguru at the gmail thingie.
-1:-- MarkEdit Conquers Markdown (Post Bicycle for Your Mind)--L0--C0--2026-04-28T07: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!