Irreal: How To Speed Up Emacs Terminals

Another great post from James Cherti. This one’s about speeding up Emacs terminal emulators. As Cherti says, Emacs terminals can be slow especially when they get pounded with a lot of output. He offers some suggestions to help.

Those changes are mostly what you’d expect. They turn off things like line wrapping that take extra cycles to calculate. Most of his post is his suggested code to remedy the slowness—although he does offer an explanations of what they’re doing—so rather than repeat what he’s already written, I’ll just send you to his post for the details.

If you’ve been bothered by slow Emacs terminal emulators, take at look at Cherti’s post.

-1:-- How To Speed Up Emacs Terminals (Post Irreal)--L0--C0--2026-09-15T15:23:44.000Z

TAONAW - Emacs and Org Mode: Glove80: First Impressions — the Typing Reset

I said I’ll do it, and I did. I got my Glove80 early! I looked at my emails yesterday afternoon, and there they were, waiting for me in the mailbox.

The uniquely-designed ergonomic split keyboard glove80. Its key sections are curved, called keywells.

I was surprised at how light the box felt, even though Xah mentioned it several times in his review. I knew it was made of plastic, but this light? Yep. The other thing that surprised me was the size: they are smaller than I thought, which in my case is excellent (I have small hands).

the glove80 keyboard parts show from the top, with a left hand positioned on the left part.

I still need to pair it (or them? as in both parts? the gloves?) with Bluetooth to both my Mac and my Linux desktop, since I only used them connected (to charge them). There is a lot to like about this keyboard, but first the big caveat: getting used to it. Them. The thing. Ugh, take your pick.

Thankfully, this was not a surprise. It’s the big issue with this keyboard: it smashes your finger memory to bits and you need to start from zero again. I couldn’t get a single sentence through my journal yesterday, and I’m typing this post here with my old Kinesis keyboard because I want to put it up today — but I’m working on it with tangent, which is designed for the Glove80 and even has an option to choose which layout I use, as it has several (I chose to stick with the default one, even though they have a macOS-dedicated one with two command keys).

I estimate it will take me about a week to 10 days of learning before I can start typing normally with it, and then another week or so to build speed and maybe add additional customizations. But that’s the main idea here: this keyboard feels right.

Even if ergonomics is not something you care about, having a keyboard that you can customize with buttons that work for your needs (and let’s be honest: Emacs, Linux and macOS come with their own unique needs when it comes to keyboards) can probably turn you into a better typist. Xah might sound a bit like a keyboard preacher, but one of the points I definitely got from him is that standard keyboards are designed after old typewriters (including the brick-like pattern of the keys), which does not make sense on modern computers. If I could go back to when I started learning to type on a computer, I would have taught myself to use columnar keyboards the same way we learn a second language.

It’s the same principle: your fingers won’t forget the old layout as long as you use it. I’m typing this on my Kinesis, and I’m fine. Meanwhile, this Kinesis keyboard is split wide apart (I have a mouse with a mouse pad separating both halves) in such a way that typing on a regular full-size keyboard (which is one board) does introduce typos I do not have on my MacBook’s keyboard, because I still use it enough. So take this to heart — I think this second language example I use is pretty spot on1.

Besides the customized buttons (which can be done with many keyboards today), I like how it’s built and shaped. It makes sense when you look at it: the keyboard should fit your hands, not your hands fit the keyboard. And while the lightness and the plastic might not be everyone’s cup of tea, I do find that I like it.

The keyboard is very easy to pack in the included case and carry with you: It practically weighs nothing, and there are no wires to stuff in the case (it’s meant to be completely wireless). It’s easy to clean, and the palm rests (not the wrist rests, which is another thing, which might introduce a problem of its own) can be wiped off easily. The keycaps come off easily if you want to replace them with others, and I’m learning to appreciate the soft, quiet keys. I might miss the heavier, clicky switches on the Kinesis, but I ask myself whether that’s more nostalgia than a real need. After all, if this keyboard were loud and clicky, I’d think twice about bringing it to the office and annoying everyone around me. But hey, to each their own.

There’s a lot to like here, but I’m definitely still in the honeymoon phase. Give me a week, and I’ll probably write more about the issues I’ve encountered and why this keyboard might not be for everyone.


  1. This is, by the way, one of the reasons I did not want to start using special modifier layers like Xah Fly Keys or god mode. These can work, but they introduce another level of learning, like another dialect of English or similar. I might get there, but first I want to learn to use the Glove80 properly. ↩︎

-1:-- Glove80: First Impressions — the Typing Reset (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-09-15T13:15:29.000Z

Protesilaos: Emacs: reply to Einar Mostad about reading the documentation

Hello Einar,

I am writing this as a public letter in response to the blog post you published on 2026-09-13 with the title An apology to Protesilaos Stavrou and David Wilson.

I accept the apology, though I think there is no need for one. I would personally not delete anything. Instead, I would add a link on top to point to the newer publication, plus some explanation. This teaches each of us to not take everything too seriously and to not pull the trigger before considering the case.

At any rate, I am fine with your choice.

Text is an especially difficult medium of communication because we do not exchange non-verbal cues. It is thus easy to imagine something which is not there and to then attribute bad intent to its author. For example, I find myself laughing at the way I formulated a certain phrase, though I can never communicate as much. Even if I write how I feel and/or what I am doing in the moment, it does not have the same effect.

Moreover, we may write something whose context is fully in our head yet is not known to the reader. Misunderstandings may thus arise.

To your point, I agree with you and have stated as much many times in video and in writing. To me, the source of truth for learning Emacs is the manual. Whatever users do to discover what Emacs has to offer, the manual remains the definitive resource.

I consider my publications complementary material, to the extent that they are informative. They effectively are a demonstration or reminder of what can be done. Though they are neither exhaustive at the time they are published nor current for every future version of Emacs. The viewer must thus be mindful to consult the official documentation.

Furthermore, Emacs is bigger than me. Even when I present my packages and/or ELisp code I have developed, I am always building on top of a corpus of work that far exceeds what I could produce in a lifetime. I mention this sometimes, but I cannot repeat it for everything I do because then I will become tiresome. Everyone should remember what is our common good in this community: Emacs.

Finally, I have warned at length against the attitude of taking shortcuts in Reply to @linkarzu about ‘What I Need From Emacs’ (2026-07-31). What I wrote on this topic is in relation to Emacs though applies to life in general. In short, we arrive quickly where we want only when we move slowly.

All the best,
Protesilaos (or simply “Prot”)

-1:-- Emacs: reply to Einar Mostad about reading the documentation (Post Protesilaos)--L0--C0--2026-09-15T00:00:00.000Z

James Cherti: Why Your Emacs Terminal is Slow and How to Fix It (vterm, eat, ghostel, term, and ansi-term)

Emacs terminal buffers like eat, vterm, ghostel, term, and ansi-term can become slow when processing large volumes of standard output. Watching Emacs freeze while a build script dumps thousands of lines is frustrating, but terminal latency is not an unavoidable cost of living inside Emacs. Applying a few targeted settings will eliminate scroll lag and restore responsiveness. This article provides a configuration that speed up terminal buffers.

Before we begin, please consider sharing this article on your website, blog, Mastodon, Reddit, X, LinkedIn, Hacker News, or other social media platforms. Sharing it will help other Emacs users discover how to improve the performance of their terminal emulators.

Complete configuration for high-performance terminal buffers

Note: You can find a description of each part of this configuration below.

The following Elisp code speeds up Emacs terminals by applying settings and disabling minor modes that are unnecessary in terminal buffers:

;; Description: Speed up Emacs terminals (vterm, eat, ghostel, term, and ansi-term)
;; Author: James Cherti
;; License: MIT
;; URL: https://www.jamescherti.com/emacs-terminal-performance-vterm-eat-ansi-term-ghostel/
(setq vterm-timer-delay 0.01)
(setq ghostel-timer-delay 0.01)
(setq eat-minimum-latency 0.007)
(setq eat-maximum-latency 0.05)

(setq vterm-max-scrollback 500)
(setq ghostel-max-scrollback (* 1024 1024))
(setq eat-term-scrollback-size (* 64 1024))

(setq eat-enable-shell-prompt-annotation nil)

;; Uncomment -DUSE_SYSTEM_LIBVTERM if you prefer system libvterm
(setq vterm-module-cmake-args
      (concat "-DCMAKE_C_FLAGS='-O3 -march=native -mtune=native' "
              "-DCMAKE_SHARED_LINKER_FLAGS='-Wl,-O2 -Wl,--as-needed' "))
              ;; "-DUSE_SYSTEM_LIBVTERM=yes"

(defun my-speed-up-terminal-buffer ()
  "Reduce unnecessary Emacs features in terminal buffers."
  (let ((ghostel-buffer (derived-mode-p 'ghostel-mode)))
    (setq-local font-lock-defaults '(nil t))

    (setq-local scroll-conservatively most-positive-fixnum)
    (setq-local hscroll-margin 0)
    (setq-local scroll-margin 0)
    (setq-local auto-hscroll-mode nil)

    ;; Uncomment to disable scroll bars to save redisplay cycles
    ;; (setq-local vertical-scroll-bar nil)
    ;; (setq-local horizontal-scroll-bar nil)

    (setq-local truncate-lines t)
    (setq-local nobreak-char-display nil)
    (setq-local bidi-paragraph-direction 'left-to-right)
    (setq-local bidi-inhibit-bpa t)

    ;; Ghostel coordinates row height calculations via ghostel-line-spacing
    (unless ghostel-buffer
      (setq-local line-spacing 0)
      (setq-local mode-line-format nil))

    (setq-local process-adaptive-read-buffering nil)
    (let ((output-max (* 1024 1024)))
      (when (< read-process-output-max output-max)
        (setq-local read-process-output-max output-max)))

    (buffer-disable-undo)

    ;; Evil users
    (remove-hook 'pre-command-hook 'evil--jump-hook t)
    (remove-hook 'post-command-hook 'evil--jump-handle-buffer-crossing t)

    ;; Disable modes
    (let ((inhibit-redisplay t)
          (inhibit-message t)
          (modes (list 'electric-pair-local-mode
                       'electric-indent-local-mode
                       'display-line-numbers-mode
                       'display-fill-column-indicator-mode
                       'hl-line-mode
                       'show-paren-local-mode
                       'flymake-mode
                       ;; Third-party packages
                       ;; NOTE: Add more modes here
                       'flycheck-mode
                       'evil-surround-mode
                       'evil-snipe-local-mode
                       'yas-minor-mode
                       'company-mode
                       'corfu-mode)))
      ;; ghostel-comint, ghostel-compile, and ghostel-links register a function
      ;; in eldoc-documentation-functions to display target URLs and file under
      ;; point.
      ;;
      ;; Ghostel uses auto-composition-mode in the sync tty composition
      ;; function.
      (unless ghostel-buffer
        (push 'eldoc-mode modes)
        (push 'auto-composition-mode modes))

      (dolist (mode modes)
        (when (fboundp mode)
          ;; Disable the mode
          (ignore-errors
            (funcall mode -1)))))))

(add-hook 'term-mode-hook 'my-speed-up-terminal-buffer t)
(add-hook 'vterm-mode-hook 'my-speed-up-terminal-buffer t)
(add-hook 'eat-mode-hook 'my-speed-up-terminal-buffer t)
(add-hook 'ghostel-mode-hook 'my-speed-up-terminal-buffer t)

Some of the setq-local settings in this function, excluding the settings that disable minor modes, are provided by modern terminal emulators such as vterm, eat, and ghostel. However, each terminal emulator modifies a different set of variables, so this function sets the relevant settings for all emulators in one place for consistency and simplicity.

I also recommend running M-x describe-mode in your preferred terminal buffer to identify any other active minor modes specific to your setup, and adding them to the list of modes to disable.

Why these settings help

The code snippet above reduces display, fontification, undo, and minor-mode performance cost in terminal buffers.

Terminal-specific configuration

(setq vterm-timer-delay 0.01)
(setq ghostel-timer-delay 0.01)
(setq eat-minimum-latency 0.007)
(setq eat-maximum-latency 0.05)

(setq vterm-max-scrollback 500)
(setq ghostel-max-scrollback (* 1024 1024))
(setq eat-term-scrollback-size (* 64 1024))

(setq eat-enable-shell-prompt-annotation nil)

;; Uncomment -DUSE_SYSTEM_LIBVTERM if you prefer system libvterm
(setq vterm-module-cmake-args
      (concat "-DCMAKE_C_FLAGS='-O3 -march=native -mtune=native' "
              "-DCMAKE_SHARED_LINKER_FLAGS='-Wl,-O2 -Wl,--as-needed' "
              ;; "-DUSE_SYSTEM_LIBVTERM=yes"
              ))
  • vterm-timer-delay, eat-minimum-latency, eat-maximum-latency, and ghostel-timer-delay: These control how terminal output is batched before redisplay. Lowering these values reduces the delay before output appears, at the cost of more frequent redraws during heavy output. These settings are worth it because they make terminal emulator feel less sluggish.
  • eat-enable-shell-prompt-annotation: Disabling shell prompt annotations avoids the processing associated with maintaining them.
  • vterm-max-scrollback, eat-term-scrollback-size, and ghostel-max-scrollback: These set the amount of history retained by each emulator. ghostel-max-scrollback is measured in bytes, vterm-max-scrollback in lines, and eat-term-scrollback-size in characters. Lowering these values prevents excessive memory consumption and reduces garbage collection latency.
  • vterm-module-cmake-args: Passes additional CMake arguments when compiling the vterm C module. The -O3, -march=native, and -mtune=native flags request aggressive compiler optimization and CPU-specific tuning. (Building with -march=native optimizes the binary specifically for your current CPU architecture. You can only run this module on machines with the same CPU architecture.)

Line rendering, wrapping, and UI

(setq-local truncate-lines t)
(setq-local nobreak-char-display nil)
(setq-local bidi-paragraph-direction 'left-to-right)
(setq-local bidi-inhibit-bpa t)

;; Ghostel coordinates row height calculations via ghostel-line-spacing
(unless ghostel-buffer
  (setq-local line-spacing 0)
  (setq-local mode-line-format nil))
  • truncate-lines: Setting this to t stops long lines from wrapping.
  • nobreak-char-display: Emacs highlights non-breaking spaces and soft hyphens by default. Setting this to nil stops Emacs from overlaying highlight boxes on top of terminal output.
  • line-spacing: TUI applications use box-drawing characters to create borders. Setting this to 0 removes vertical pixel gaps between lines, ensuring pixel-perfect vertical alignment for TUI borders and progress bars.
  • bidi-paragraph-direction and bidi-inhibit-bpa: Emacs scans text by default to determine if it should be rendered right-to-left. Setting bidi-paragraph-direction to 'left-to-right and bidi-inhibit-bpa to t forces left-to-right rendering. This bypasses expensive text scanning. Right-to-left languages will render incorrectly in the shell.
  • mode-line-format: Setting this to nil hides the mode-line. This stops Emacs from constantly re-evaluating mode-line functions (which check Git status or active modes) on terminal buffers.

Accelerating display and scrolling

(setq-local scroll-conservatively most-positive-fixnum)
(setq-local hscroll-margin 0)
(setq-local scroll-margin 0)
(setq-local auto-hscroll-mode nil)

;; Uncomment to disable scroll bars to save redisplay cycles
;; (setq-local vertical-scroll-bar nil)
;; (setq-local horizontal-scroll-bar nil)
  • scroll-conservatively: Setting this variable to most-positive-fixnum makes Emacs scrolls only enough to bring point into view rather than recentering.
  • hscroll-margin and scroll-margin: Define the horizontal and vertical padding around point before scrolling occurs. Setting them to 0 disables this padding, preventing the display engine from performing recentering calculations when point nears the window edge.
  • auto-hscroll-mode: Disallows automatic horizontal scrolling of windows.
  • vertical-scroll-bar and horizontal-scroll-bar: Disables the scroll bars. Every time new text is inserted, the redisplay engine recalculates the size and position of the scroll bar thumb based on the new buffer size. Disabling scroll bars removes the scroll-bar-related redisplay and UI work for the terminal buffer.

Process I/O and buffering

(setq-local process-adaptive-read-buffering nil)
(let ((output-max (* 1024 1024)))
  (when (< read-process-output-max output-max)
    (setq-local read-process-output-max output-max)))

Increasing read-process-output-max allows Emacs to read up to (* 1024 1024) amounts of output in a single system call. However, a larger chunk size alone is not enough because the default process-adaptive-read-buffering algorithm will still inject micro-sleeps, assuming rapid bursts of escape sequences are caused by a background process that needs throttling.

Ensuring process-adaptive-read-buffering is disabled forces the event loop to stop second-guessing the data stream and immediately consume those chunks as fast as the operating system provides them. (Adaptive read buffering is disabled by default. This is included in case the global value has been modified in your Emacs configuration.)

Disabling syntactic fontification

(setq-local font-lock-defaults '(nil t))

Setting font-lock-defaults to (nil t) turns off automatic fontification for the buffer. The first element specifies that there are no font-lock keywords, while the second sets font-lock-keywords-only, which prevents syntactic fontification.

This is useful for terminal buffers because terminal emulators do not generally need Emacs to apply programming-language fontification to their contents. Terminal applications manage their own colors through terminal escape sequences.

Disabling undo history

(buffer-disable-undo)

Disables recording undo data for the buffer.

Managing Evil mode hooks (Evil-mode users)

(remove-hook 'pre-command-hook 'evil--jump-hook t)
(remove-hook 'post-command-hook 'evil--jump-handle-buffer-crossing t)
  • evil--jump-hook and evil--jump-handle-buffer-crossing: These hooks are used by Evil mode to calculate and store jump list entries on every command. Removing them bypasses the computation and storage of jump list entries for every single command executed in the terminal. The downside is that Evil mode users cannot use C-o or C-i to return to a previous cursor position in terminal buffers.

Disabling intrusive minor modes

;; Disable modes
    (let ((inhibit-redisplay t)
          (inhibit-message t)
          (modes '(electric-pair-local-mode
                   electric-indent-local-mode
                   display-line-numbers-mode
                   display-fill-column-indicator-mode
                   hl-line-mode
                   show-paren-local-mode
                   flymake-mode
                   ;; Third-party packages
                   ;; NOTE: Add more modes here
                   flycheck-mode
                   evil-surround-mode
                   evil-snipe-local-mode
                   yas-minor-mode
                   company-mode
                   corfu-mode)))
      ;; ghostel-comint, ghostel-compile, and ghostel-links register a function
      ;; in eldoc-documentation-functions to display target URLs and file under
      ;; point.
      ;;
      ;; Ghostel uses auto-composition-mode in the sync tty composition
      ;; function.
      (unless ghostel-buffer
        (push 'eldoc-mode modes)
        (push 'auto-composition-mode modes))

      (dolist (mode modes)
        (when (and (boundp mode)
                   (fboundp mode))
          (ignore-errors
            (funcall mode -1)))))
  • yas-minor-mode, evil-snipe-local-mode, evil-surround-mode: These modes intercept keystrokes for snippets or character searches. Disabling them bypasses extra keymap lookups and event loop interceptions, reducing latency between keystroke and terminal input.
  • hl-line-mode, display-line-numbers-mode: These modes alter the visual representation of lines. Disabling them prevents the display engine from removing and recreating overlays on every cursor movement or calculating margin widths for every visible line.
  • electric-pair-local-mode, electric-indent-local-mode, show-paren-local-mode, auto-composition-mode: These modes analyze syntax to insert quotes, indent lines, or highlight brackets. Disabling them stops Emacs from running complex regex, syntax table lookups, and text-shaping on every typed character or newline, which speeds up text rendering.
  • company-mode, corfu-mode, flymake-mode, flycheck-mode, eldoc-mode: These modes manage completion, linting, and documentation lookups. Disabling these modes removes their associated hooks, redisplay work, buffer analysis, completion, diagnostics, or other processing from terminal buffers.
-1:-- Why Your Emacs Terminal is Slow and How to Fix It (vterm, eat, ghostel, term, and ansi-term) (Post James Cherti)--L0--C0--2026-09-14T15:00:45.000Z

Irreal: PSA: Homebrew Updated To Version 7.0.0

If you’re a macOS user, you probably know about Homebrew, a repository of easy to install apps and libraries. I use it all the time because it’s so much easier than hunting up all the dependencies required for compiling them yourself.

If you’re a Linux user, you may or may not be familiar with Homebrew. It provides the same services for Linux but my—possible wrong—impression is that its Linux take-up is not as great as it is in macOS.

Regardless, if you’re on Linux or macOS, you should definitely check it out. The TL;DR of this post is that Homebrew has just released version 7.0.0, a major release. That probably wouldn’t be worth an Irreal post except for the “major release” part. On the macOS side, macOS 10.5 and earlier are no longer supported. Being a Mac-head, I didn’t check discontinued versions on Linux so if you’re using Homebrew on Linux you should check that out.

The other big change for Mac-heads is that Intel Macs are moved to Tier 3. You can check out the announcement for exactly what that means but basically it indicates that support is limited and end-of-life is approaching (in September of 2027).

Homebrew is an excellent repository and well worth investigating if you’re not familiar with it. I still have an Intel Mac so it’s a bit of a pain that support is diminishing but I need to upgrade anyway so this is probably the push I need.

-1:-- PSA: Homebrew Updated To Version 7.0.0 (Post Irreal)--L0--C0--2026-09-14T14:55:23.000Z

Sacha Chua: 2026-09-14 Emacs news

Not much commentary this week, but plenty of cool stuff to explore!

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

View Org source for this post

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

-1:-- 2026-09-14 Emacs news (Post Sacha Chua)--L0--C0--2026-09-14T14:09:11.000Z

Lars Ingebrigtsen: Emacs, movies and SQLite

Hey, it’s the… *counts on fingers*… 22nd anniversary of me using Emacs to watch TV and movies!

And in the start, it was a snappy, focused package that just listed directory contents and let me play the files… but then it grew to take care of resuming where I left off, and fixing the frame rate of the TV to fit the video file, and lately it’s also automatically choosing the correct subtitles and audio track.

This means that it’s now calling mediainfo before playing the file to get all that data. And since it’s accessing the files over nfs, and mediainfo apparently has to load the entire file (or large parts) to cough up that data, things have gotten slower:

Look at that! It’s horrible! I have to wait, er, two seconds before I can watch Saturday Night Live?! How can anybody live like this?

Now, sensible video player interfaces compute all this stuff when they incorporate a file. But I didn’t want to do that, because just having some directories of random files that I can move around without doing any synchronisation is just so convenient.

And it worked without that delay until I added the auto-subtitle selection thing.

But… one thing I’ve had to precompute anyway are the thumbnails, and I’ve just plopped them into the directories, which is icky:

Having the video player mangle the source directories like this is not ideal at all. I mean, I don’t look at the mess myself — the Emacs interface hides all this junk — but I know it’s there! *gasp*

So… taking all that into account (the new slowness and this yuckiness) I thought it was time, after 22 years, to bite the bullet and do things the traditional way. I.e., prescan the files and stash everything in an SQLite database. (And besides, the “use no database” thing fell apart very quickly — the file for keeping track of progress was a database file anyway, just very primitive.)

Tada!

Look how fast? Ish. Fastish. mpv still has to load the start of the file over nfs, so it’s not instantaneous, but it’s a fraction of a second, so that’s OK.

And look how pretty:

Putting everything into an SQLite database also means that I can keep track of more data. Previously I only kept track of whether I’d seen something, and how far I’d watched something (so it could resume automatically), but now I can also keep track of how many seconds I’ve watched something, for instance. (Skipping to the ten minute point and then abandoning isn’t the same as watching for ten minutes and then abandoning.)

And I think I still can keep my “just move files around” (to a “rainy day” directory, for instance) workflow — I store file hashes, so even if they move around, they’re recognised when they’re re-encountered.

But now for the real test — my kitchen computer. I use it to watch Star Trek while cooking, and it mounts the /tv/ directory over sshfs over wifi! (The TV computer mounts the directory over nfs over Ethernet, so it has much less latency.) Will it be able to access the SQLite database in a reasonable amount of time?

Yowza! That’s speedy! The only system, which ran mediainfo on the file before starting to play, was very, very slow — it took multiple seconds. The new thing is, like, a million percent faster! That’s bigly faster!

And with all the data stashed in a more convenient way, it’s easier to whip up stuff like this, which answers “when did I last watch DS9, and for how long a time?” (The answer is — “now”, and “not very”.)

The code is on Microsoft Github, for those that are curious.

-1:-- Emacs, movies and SQLite (Post Lars Ingebrigtsen)--L0--C0--2026-09-14T13:16:14.000Z

James Dyer: Emacs 31 Does Git Worktrees Natively: How My vc Setup Compares

Well this is awkward timing. No sooner had I finished bolting worktree management onto built-in vc than I installed the bleeding edge and found Emacs 31 went and did it properly. Right there in the 31 NEWS: a whole C-x v w working-tree map. So before I get too attached to my own code I thought I had better do an honest head-to-head. Verdict up front: I am keeping mine, with one key moved. Here is why.

20260907140012-emacs--Emacs-31-Native-Git-Worktrees-vs-My-vc-Setup.jpg

First, what 31 actually ships under C-x v w:

  • w c (vc-add-working-tree) prompts for a directory, then a branch, but you have to create a branch for yourself, where as my version and also Magit ask for the name of the branch to create, as when creating a new worktree, wouldn't you always want to create a branch? Also with the built-in there is no placeholder initial name, with something involving the branch name, so this command currently feels a little disjointed.
  • w w (vc-switch-working-tree) jumps to the same file in another tree. Lovely idea, completely different from my switch-to-vc-dir and I'm not quite sure if I really care about switching file contexts, I think I just really want to jump to the top level vc-dir
  • w k (vc-kill-other-working-tree-buffers) kills buffers visiting this file elsewhere. Read that twice: it does not delete the tree, despite the letter. So these commands are really very much file-centric. Again I would rather this delete the worktree.
  • w s (vc-working-tree-switch-project) is project-switch-project limited to trees sharing the same backing repo, with a matching project-find-matching-buffer helper for hopping between same-named files, for this I would probably generally be more specific.
  • w a and w A (vc-apply-to-other-working-tree and vc-apply-root-to-other-working-tree) copy a fileset's changes - or the whole tree's - into another tree, and with a prefix argument move them. This is the Transplant mechanic and it is genuinely something mine cannot do, do I need it, not sure, but I have logged it into my noggin.
  • w x (vc-delete-working-tree) deletes the directory outright (to trash) and prunes. No --force dance because there is no git worktree remove involved at all, which also means it works on ancient gits.
  • w R (vc-move-working-tree) relocates, and the implementation is cleverer than mine: on recent git it renames the directory and runs worktree repair, which means it can even move the main tree, something plain git worktree move flatly refuses. Older gits fall back through worktree move to a clean version error. A little on git worktree repair, the .git file contains a repo reference and is backtraced through the original repo, the repair just reset both aspects which is very useful when moving worktrees around and also in that small case where you are running a shared folder repo over SMB, to get the worktree to work in both linux and maybe a host on windows!!

Now the head-to-head, feature by feature. Creating: theirs prompts bare directory then branch to use, but the branch must already be created, mine offers a project-branch sibling default and lands you in vc-dir.

Switching: theirs is file-grained, mine is status-buffer-grained - complementary, not competing. Listing: theirs has no list command at all beyond completion prompts (though the backend exposes vc-git-known-other-working-trees for lisp), while my Z l buffer stays.

Pruning: no built-in command, mine stays. Moving: theirs is strictly better, full stop.

Deleting: theirs is safer on old gits and trashes instead of destroying; mine understands --force and rescues a vc-dir sitting inside the dead tree.

So much for behaviour; now keys, because this is where it bit me. My maps live on Z (worktrees, Magit-style). Against 31's C-x v w map: w k kills buffers where mine removes trees, and w s switches projects where mine opens vc-dir - same letters, different continents. That settled it: the Z prefix stays, precisely because w is now taken with conflicting semantics, and note vc-dir itself has no built-in worktree keys at all, so Z there is uncontested territory.

What I am stealing, or at least circling: the prune-before-listing habit is obviously right, the trash-first delete is kinder than --force bravado, and the repair-based move handling the main tree is something git worktree move simply cannot do. The changeset transplant (w a) is the one I may just use as-is rather than reimplement - some things are better borrowed than rebuilt. What I am not giving up: the vc-dir-first workflow, the sibling defaults, the list buffer.

-1:-- Emacs 31 Does Git Worktrees Natively: How My vc Setup Compares (Post James Dyer)--L0--C0--2026-09-14T12:42:00.000Z

Irreal: Optimizing Line Numbering

James Cherti has been busy producing a lot of posts about optimizing various aspects of Emacs behavior. I’ve just come across his post that discusses optimizing buffer line numbering. It explains the different line numbering schemes and their relative costs.

I not a big user of line numbering. I usually turn it on for the Elfeed index but hardly every use it otherwise unless I have a specific need. In particular, I hardly ever use them in code buffers because I prefer to jump to a particular construct rather than a line number, which, irrationally, always seem to me to be ephemeral.

When I do use line numbers, I like absolute line numbers rather than the two types of relative numbers. That turns out to the right choice as far as efficiency is concerned but many people—including Cherti—prefer relative numbering. That can be expensive, especially for long lines, but there are some settings that can speed things up.

The descriptions of those settings don’t really capture what they do and why you might prefer one value over another so Cherti’s post is especially helpful. If you’re one of the people who like having your buffers numbered in one way or another, take a look at Cherti’s post to see how to get the best possible performance for your choice of representation.

-1:-- Optimizing Line Numbering (Post Irreal)--L0--C0--2026-09-13T15:57:59.000Z

Einar Mostad: An apology to Protesilaos Stavrou and David Wilson

A while back, I read an Emacs News post by Sacha Chua and saw that the Emacs carnival for the month was about mistakes done early in one's Emacs journey. I read this one of the last days of the month, late in the evening, and thought I would chime in before the next theme in the carnival. So I wrote and posted a blog post fast, late at night.

Later when I reread what I had written, I realised it did not say what I was trying to say and instead insulted two brilliant contributers to the Emacs community, so I deleted the post. I have felt bad about it ever since, and I feel that I owe the people I insulted an apology.

What I was trying to say was that I should have spent more time early on to read the built-in documentation in Emacs. If I looked more into the built-in documentation of variables, functions, keyboard shortcuts and read a bit more in the manual, it would have given me a more comprehensive understanding of how Emacs works. I learned a lot by watching videos, but in retrospect, I wish I had also found the time to read up more.

Unfortunately, the blog post read as if I was saying that I should not have watched as many videos by Protesilaos Stavrou and David Wilson as I did, which wasn't really what I meant. I apologise to David Wilson and Protesilaous Stavrou!

Before I started using Emacs, I watched a video by Protesilaos Stavrou who told me that Emacs is an integrated environment that can be tailored to the user's need with less context switching than using [Neo]Vim plus CLI and TUI programs which was what I did at the time. Emacs seemed like something I would like to try, and I have Protesilaos to thank for getting the right frame of mind of what Emacs is even before starting to try it out!

When I started trying out Emacs, I did the Emacs tutorial and then I watched videos by Protesilaos Stavrou and David Wilson that guided me forward in my journey. David Wilson's "Emacs from scratch" series taught me a lot about configuring Emacs early on and Prot's videos on dired, eww, elfeed, diary and calendar taught me about specific modes. These videos made the confusing early days of using Emacs a lot less confusing and a lot more enjoyable!

I have continued to watch their videos and they have given me a lot of useful information and lots of ideas of workflows, modes to explore and settings to use to make Emacs both more efficient and more pleasant to use. I have also enjoyed David Wilson's SystemCrafters videos on Guix. I have learned a lot by watching their videos and I am very grateful for their work!

-1:-- An apology to Protesilaos Stavrou and David Wilson (Post Einar Mostad)--L0--C0--2026-09-13T14:53:00.000Z

Irreal: Bedrock 2.0

Three years ago, I wrote about the Bedrock starter kit for Emacs by Ashton Wiersdorf. Now Wiersdorf has issued a new major release of the kit. It’s mainly an update for Emacs 31 and requires that version to run. I liked his announcement and description of Bedrock 2.0 and decided to write about it on Irreal. To be honest, I’d forgotten that I’d already written about Bedrock and didn’t discover that I had until I chose the obvious name for the post source file (bedrock.org) and found that such a file already existed.

You should read what I wrote before; everything from that post applies to the new version. The TL;DR is that although I don’t generally care for starter kits, I do like Wiersdorf’s because it mainly concerns itself with simple variable settings rather than configuring a bunch of packages that a user may or may not want1. His init.el is well commented to explain what everything does and some items are commented out with an explanation of why you might want to enable them. You can see the file here.

If you’re looking for a simple, minimal configuration for Emacs as a starting point or you know a n00b who is, take a look at Bedrock.

Footnotes:

1

There are, as with the original version, some auxiliary files that bring in some packages that Wiersdorf finds particularly useful.

-1:-- Bedrock 2.0 (Post Irreal)--L0--C0--2026-09-12T15:34:45.000Z

James Cherti: Fixing Slow Scrolling in Emacs display-line-numbers-mode

The display-line-numbers-mode and global-display-line-numbers-mode modes render line numbers in the margin of an Emacs window. Line-number display is implemented in the core C display engine, avoiding the performance cost of legacy Elisp overlays like linum-mode. Despite the native C implementation, and even after configuring Emacs scrolling for better usability, specific configurations can still trigger expensive Lisp evaluations during the interactive command loop, vertical scrolling, and buffer initialization.

These settings avoid unnecessary width calculations and initialization scans.

Speed comparison across line-number types

The display-line-numbers-type variable specifies the type of line numbers activated by display-line-numbers-mode and global-display-line-numbers-mode. It accepts three values:

  • Fastest: t (Absolute): Displays the absolute line number corresponding to the physical buffer line. This is the fastest rendering method because the values are static and do not require recalculation when the cursor moves.
  • Fast: 'relative: Displays the line number relative to the current cursor position (point). This is slightly slower than absolute line numbering because the display engine must recalculate the distance for every visible line on each vertical cursor movement.
  • Slow: 'visual: Displays relative numbers based on visual screen lines, accounting for wrapped text. This is the slowest rendering method because it forces the display engine to compute line wrapping layout before assigning numbers.

Performance also depends on the buffer contents and display configuration. For example, using 'visual numbering can degrade scrolling performance in buffers with long logical lines and word wrapping enabled (when truncate-lines is set to nil), because the display engine must calculate layout metrics to determine screen-line boundaries.

I recommend choosing the numbering type based primarily on the desired behavior, not just performance. For instance, even though absolute line numbering is the fastest, I prefer 'relative numbering. Relative numbers show the distance from the current line, which can make vertical navigation easier, especially with numeric prefixes such as C-u 15 C-n in vanilla Emacs or 15j in Evil mode.

;; Enable relative line numbers globally
(setq display-line-numbers-type 'relative)

Disabling display-line-numbers-grow-only

Avoid setting display-line-numbers-grow-only to a non-nil value, as doing so adds display-line-numbers-update-width to the buffer-local pre-command-hook. (The display-line-numbers-update-width function checks the required line number width before each command and increases the display-line-numbers-width variable when necessary.)

Leaving this variable nil avoids that additional Lisp function call before each command:

;; Do not attach the line number width update function to pre-command-hook
(setq-default display-line-numbers-grow-only nil)

Adjusting the display line numbers width

The display-line-numbers-width variable specifies the minimum width of the line-number area. If the required width exceeds the specified value, Emacs can allocate more space.

(setq-default display-line-numbers-width 3)

This sets a minimum width for the line-number area. Without a predefined width, the display engine can adjust the margin width as line numbers grow during vertical scrolling, such as when the line number changes from 99 to 100.

Disabling the initial line-count scan

The display-line-numbers-width-start variable controls whether Emacs calculates an initial width based on the number of lines in the buffer.

Avoid setting display-line-numbers-width-start to a non-nil value because Emacs then scans the entire buffer when initializing the mode.. When enabled, this setting calls a synchronous (count-lines (point-min) (point-max)) evaluation to compute the total line count before establishing the initial margin width.

For large buffers, keeping this disabled avoids that initialization-time count-lines operation:

;; Do not calculate the initial line number width from the buffer's line count
(setq-default display-line-numbers-width-start nil)
-1:-- Fixing Slow Scrolling in Emacs display-line-numbers-mode (Post James Cherti)--L0--C0--2026-09-12T15:27:23.000Z

Sacha Chua: Experimenting with Orukeet for speech recognition

This is a quick demonstration of using Orukeet as a speech recognition model inside Emacs. I'm recording this in real time on a Lenovo P52 laptop using only CPU. I can replay just one part of my recording. I can switch languages in the middle of recording too. Par exemple, maintenant je parle français. Seems promising.

Links/notes:

View Org source for this post

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

-1:-- Experimenting with Orukeet for speech recognition (Post Sacha Chua)--L0--C0--2026-09-12T13:39:22.000Z

Marcin Borkowski: Duplicating lines and other Emacs miscellanea

A few weeks ago I wrote about how I remapped yank to my command which allows to transform the yanked text. The whole setup – where I press C-y twice to trigger the transformation transient – has one drawback: I sometimes legitimately want to call yank twice in a row. This happens when I need to copy the current line and modify the copy so that I have two similar lines in a row. (I often need this when editing Ledger transactions, for example.) What I sometimes do in such a situation is basically a dance of C-a C-1 C-k C-y C-y. It turns out I don’t need to.
-1:-- Duplicating lines and other Emacs miscellanea (Post Marcin Borkowski)--L0--C0--2026-09-12T06:11:40.000Z

Lars Ingebrigtsen: Spellchecking Total Eclipse with an AI

I’ve spent a day test-driving that Emacs minor mode for spellchecking using an LLM that I wrote the other day. I went through all the posts on the Total Eclipse blog (all 260 of them) to see whether the thing works or not.

And, yes, it works now, but it took some tweaking. Well, I rewrote the mode.

Twice.

My original approach was to give the LLM the entire text, and then ask it to return it to me verbatim, but with changed words tagged up specially. This worked 90% of the time, but it would randomly decide to do something else… and I was never totally sure that it actually returned the entire text. That approach was a bit too nerve-racking.

But Sacha Chua has done something along similar lines, and her approach is to ask the LLM to return just the changed parts:

That sounds like a much more responsible approach, so I redid my package to do pretty much the same… but no matter how I phrased things in the prompt, I couldn’t get the LLM to follow my instructions more than 50% of the time, really.

The hard thing to make it (I’m using Sonnet 5) understand is that it should return a regexp that identifies the fragment uniquely, and that the word to be replaced should be inside a \(…\) group. But:

Some regexps would have zero grouping operators, and some would have several. And sometimes it would wrap the JSON in Markdown:

And worst of all, I could not get it to understand even once that it shouldn’t do this:

I.e., it seems like the LLM did the “regexps” by slapping a (…) around the term to be replaced, and then it put two backslashes before every parenthesis in the string. This is wrong.

So I redid the thing once again, but without regexps. Instead I asked it to return some JSON of the offending phrase (with enough context to uniquely identify it), and what it wants to replace it with:

And then I compute myself the actual difference between the phrases for markup purposes.

After this change, the LLM hasn’t screwed up the format once.

I guess what this really drives home for me is that the apparent enormous strides LLMs have taken the last six months for programming are mostly an illusion: You can now ask an LLM to whip you up, say, a Python script, and it chugs away at it, and then gives you a Python script that’s syntactically correct and mostly does what you asked.

But this isn’t because the LLMs now can generate correct syntax flawlessly. No, this is because the LLMs now have a sandbox they test-run all scripts before handing them over. So they discover that what they’ve generated is junk, and then iterate rapidly over it until it works.

But when I’m using the API here, it can’t check whether the regexps I asked it to generate are valid. It spits out something that’s close, but if it had its normal interactive harness, it would have tested the regexps and seen that they were invalid, and then fixed them.

So… it’s the LLM tooling that tricks us into believing that LLMs are pretty smart at computer stuff now. They aren’t, really.

Anyway, the Total Eclipse blog should now be 100% without typos! Woohoo! And since I’ve eyeballed every change, hopefully the LLM hasn’t changed too many instances of “important” to “load-bearing” without me noticing.

(One amusing thing I’ve noticed is that you can run the same article through the LLM several times, accepting the edits each time. As it finds fewer and fewer real things to complain about, it gets more and more nitpicky. But eventually it gives up and says “nothing to be changed”.)

-1:-- Spellchecking Total Eclipse with an AI (Post Lars Ingebrigtsen)--L0--C0--2026-09-11T20:46:21.000Z

Bicycle for Your Mind: Traverse Through Outlines

traverse icontraverse icon

Traverse | OPML Outliner for macOS

Price: Traverse Lifetime: $17.99. Annual Subscription: $9.99. Monthly Subscription: 0.99

TL;DR::

Traverse is a competent outliner which deals in OPML, Markdown and Fountain files. It is well-designed with a column view which is addictive.

OPML All The Way

Traverse saves its files in the OPML format. “OPML (Outline Processor Markup Language) is an XML format for outlines (defined as”a tree, where each node contains a set of named attributes with string values”). Originally developed by UserLand Software as a native file format for the outliner application in its Radio UserLand product, it has since been adopted for other uses, the most common being to exchange lists of web feeds between web feed aggregators.”[^https://en.wikipedia.org/wiki/OPML]

What that means to the user is “no lock in.” Every outliner in the market place should be able to open and work with OPML files and most do. OmniOutliner, Kosshi, Bike, and Opal are all able to work with OPML files. If at any point, you stop using Traverse, you can use a different outlining program and you have complete access to the files produced by Traverse.

An Unique Outliner

traversetraverse

Traverse is unique in the marketplace. It has a mix of features which are not shared by any outliner in the marketplace. These are some of the ways in which Traverse is different:

  1. It gives you four views into the same outline. There is a regular outline view. This is the one most outlining programs have implemented. There is a Writer view, where those coming from a text editor background will find most familiar. Columns Auto view, which is what the users of the outliner Tree (this seems to have disappeared from both the Apple store and the Internet), might be used to. A variation of that is the Columns Manual view. Four views into the same outline. What that means for the user is that depending on the kind of outline you are making, or the kind of writing you are doing, you have the right environment for the task. I am writing this review in the Writer view.
  2. Traverse does not have its own file format. It saves everything in the OPML format. That is an industry-standard format for outlines and easily dealt with by other outlining programs.
  3. It incorporates a whole system of templates and snippets. Templates to start a new document with a predetermined structure which you have defined. Snippets to add a proven piece of structure to sections in your documents.
  4. Traverse imports, writes and exports Fountain, the screen-writing format. I don’t know any other outliner which does that.

Preferences

Traverse is being worked on at a rapid pace and the screenshots might be out of date as the developer tweaks the product. This is a general idea of what preferences you can set for Traverse.

traverse appearancetraverse appearance

This is the way you can customize the look of the program. It doesn’t have themes but it does have a light and dark mode. The only problem I have with it is the font size. Why 20 point? Why not give the user the ability to set an actual font size.

traverse writingtraverse writing

There is a nice formatting toolbar when you select text. These are my settings. You can change them to suit your way of working.

traverse keyboardtraverse keyboard

I am fond of this panel. I have years of experience with outliners, which means that my fingers have memory. Traverse gives me the ability to customize all of the keyboard commands to suit my muscle memory. This makes it easier to adopt a new application in this category.

traverse new outlinestraverse new outlines

You can set some options for new outlines you create. I like Writer view. There are some other properties you can set.

traverse aitraverse ai

For those of you interested, there is a link to Apple Intelligence. I have it turned off.

traverse exporttraverse export

You get to control export functions in Traverse. I deal with Markdown Documents from Traverse. That is the panel I am showing in this screenshot.

traverse librarytraverse library

traverse library iCloudtraverse library iCloud

You get to choose where you put your outlines and there is iCloud integration, if you need that.

Imports and Exports

traverse importstraverse imports

Traverse imports OPML, iThoughts, Tree, Plain Text, Markdown and Fountain files.

traverse exportstraverse exports

It exports OPML, Markdown List, Markdown Documents, Fountain (I have that turned off), CSV, and the current view as PDF. Also lets you stream the content to Marked.

Using Traverse

Traverse comes across as a simple outliner, but there is power hidden under that façade. It does a good job of providing you with as much complexity as you want to endure. It is fast, and fairly stable. The developer, Stuart Marshall, is responsive and quick to respond to both bug fixes and feature requests.

I have been enjoying the Writer view and the outline view of the application. It supports typewriter scrolling (thank you).

The Competition

Suddenly we have choices in the outliner category in the macOS space. OmniOutliner, Zavala, Kosshi, Bike, and now Traverse are all playing in this space. Zavala is distinguished by being free. OmniOutliner is expensive but the standard in this category. Kosshi is simple, and capable. Bike is fairly mature and working towards the next version. Traverse sits somewhere in the middle of this spectrum. It does some things which no one else does and that is what makes it special.

For me, the outlining category is now served by Org-mode in Emacs. If I want to get away from Emacs and need a change of scenery, I find myself reaching for Kosshi. The urge to go write somewhere else is usually triggered by me temporarily not wanting to deal with the complexity of Emacs. Kosshi fits the bill for that retreat. It is simple and responsive. I am comfortable there. I can see spending time with Traverse and getting comfortable there too. It is well-designed software.

Suggested Improvements

More typographic controls would be welcome. Spaces between lines and paragraphs would enhance the writing experience. Please let us set the font size ourselves.

Conclusion

If you haven’t tried out an outliner and are looking for a comprehensive, well-designed solution, Traverse will not disappoint.

If you are missing the tree view of Tree (the disappeared outliner), Traverse is the only available alternative.

I recommend Traverse wholeheartedly.

Resources

Help Center

Category Traverse posts | Shaking the Habitual

Writing Screenplays with Fountain in Traverse | Shaking the Habitual

macosxguru at the gmail thingie.

Note: The developer provided a promo code for the program when I asked for one.

[^1: https://en.wikipedia.org/wiki/OPML]

-1:-- Traverse Through Outlines (Post Bicycle for Your Mind)--L0--C0--2026-09-11T07:00:00.000Z

Meta Redux: Clojurists Together Update: July and August 2026

Time for another bi-monthly update on the work that Clojurists Together are funding this year - my maintenance of nREPL, CIDER and friends. I published the previous one as a blog post for the first time and the feedback was good enough that I’ll keep doing it.

Last time I said that I had plucked most of the low-hanging fruit and that the next two months were unlikely to be as productive. Well, I was wrong. CIDER 2.0 finally shipped, and once it was out the door I used the momentum to sweep through pretty much every corner of the nREPL/CIDER ecosystem. A few long-neglected projects got proper releases, and nREPL got a couple of brand new implementations in languages I play on the side from time to time.

The big highlights from my perspective:

  • CIDER 2.0 (“Terceira”) is out, followed by 2.0.1, and 2.1 is taking shape on master
  • clj-refactor 4.0 is out
  • Sayid went from 0.4 to 0.8 in the span of three weeks
  • Drawbridge, nREPL’s HTTP transport, got its first meaningful release in years
  • nREPL went polyglot: nREPL servers for Erlang and Elixir and an OCaml client
  • clj-suitable 0.7 and 0.8 closed most of the gap between ClojureScript and Clojure completion
  • A lot of work landed on nREPL’s master (TLS hardening, URL-based connections, docs) and a new release is right around the corner

Below you’ll find more details about the work I did, project by project.

CIDER

CIDER 2.0 (“Terceira”) landed on July 15, right on the schedule I had announced in the preview post. For once in my life I was actually on time! The big themes were covered there and in the release announcement (transient menus, inline macro stepping, call-graph browsers, source-based find-references, the tracing and tap buffers, rich content in the REPL), so here’s just what changed between the preview and the final release:

  • cider-doctor, which checks your Emacs setup and the active nREPL session for common problems and produces a report you can paste in a bug report
  • an orchard value for cider-print-fn, selecting cider-nrepl’s much faster orchard.pp pretty-printer
  • SSH tunnels now forward a free local port, so remote REPLs sharing a port no longer collide on localhost
  • C-c C-d at the stdin prompt sends end-of-input, and stdin is routed to the exact connection that asked for it
  • a long tail of nREPL client fixes: a slow memory leak on the eldoc/completion path, nrepl-dict-merge mutating a shared literal, notifications treated as format strings

CIDER 2.0.1 followed a week later with fixes for the problems early adopters ran into: evaluation in a dependency’s source buffer erroring with “No linked CIDER sessions” (in several variants), cider-enlighten-mode never lighting anything up (a 1.22 regression), the macroexpansion commands refusing to expand let/fn/loop, and load-file potentially freezing Emacs on a huge result. Nothing dramatic, but I’m glad people were quick to report those.

After that master (the future CIDER 2.1) kept moving at a steady pace. A few of the things that landed there:

  • CIDER’s dynamic font-locking (REPL-defined macros, functions, deprecated/instrumented/traced symbols) now works better in clojure-ts-mode buffers via tree-sitter. Previously it worked “officially” only under clojure-mode. The debugging reader tags are highlighted there too.
  • A new cider-preferred-clojure-mode controls which mode CIDER uses to font-lock the code it renders - REPL results, doc examples, overlays and its own display buffers. clojure-ts-mode is finally a first-class citizen in CIDER.
  • Symbol prompts can go through completing-read (so Vertico/Ivy/Helm kick in) and completion annotations render as an aligned type/namespace column in Corfu, Vertico and the built-in *Completions*. More on this in Modernizing CIDER’s Completion.
  • Connecting got smarter. Container-published nREPL ports are resolved for /docker: and /podman: buffers, lein trampoline REPLs are detected, .nrepl-port files are no longer discarded on systems without lsof, and there’s a new “How CIDER Finds Ports” section in the manual.
  • Every form command got an “at point” variant (inspect, pprint, macroexpand, format, insert in REPL), there’s a cider-inspect-menu listing every way to start an inspection, and the contents of comment forms are treated as top level by the whole defun command family.
  • Stray output from long-lived background processes (say, a core.async go-loop still printing under a finished eval’s id) is now routed to the REPL instead of being dropped with a warning.

One more thing. I shipped “smarter form targeting” on master - the evaluation commands resolving the form from where the cursor actually is, rather than the form before it - wrote about it, got a lot of feedback, and reverted it a few days later. CIDER 2.1 will keep the classic Emacs semantics. Fifteen years in, the existing behaviour is the contract, not an implementation detail I get to tidy up. The detour wasn’t wasted, though: it surfaced a bug where the text of a line comment was treated as code, and the “at point” family of commands is a direct result of it.

cider-nrepl

Three cider-nrepl releases in July, wrapping up the tools.deps migration and driving the CIDER 2.0 launch:

  • cider-nrepl 0.62.0 finalized the Leiningen to tools.deps migration, simplified deferred middleware loading, documented the op response keys (with a test verifying the descriptor contract) and shipped the hardened content-type and slurp middleware that made rich content safe to enable by default.
  • cider-nrepl 0.62.1 fixed a whole cluster of debugger bugs. Record literals no longer get downgraded to plain maps by instrumentation, deftype/defrecord method bodies are skipped (goodbye Unable to resolve symbol: STATE__), and enlightening deftest bodies works again.
  • cider-nrepl 0.62.2 pruned trace and tap subscriptions with dead transports (a dead subscriber used to break every traced evaluation), stopped the debugger from shadowing enlighten’s evaluator, brought the docs back in sync with the code and added a section for tool authors.

Orchard

Orchard 0.44.0 shipped on July 4, mostly thanks to Sashko’s inspector work (a replace command, truncated table columns, ARef contents rendered fully). My part was a round of tests for the less covered namespaces and, later on master, a fix for orchard.print ignoring custom print-method implementations for records and collections. Thanks, Sashko!

clj-refactor 4.0

clj-refactor.el 4.0 is the release I had been promising for a few cycles. It requires Emacs 28.1+ and CIDER 2.0+, and it’s a big one:

  • project-wide refactorings (rename symbol, change signature, inline symbol) now show a diff preview before touching disk, and cljr-undo-last-refactoring reverts the last one in a single step
  • the slow refactorings run asynchronously, so Emacs no longer freezes while the middleware analyzes the project
  • cljr-change-function-signature can add and remove parameters and handles multi-arity functions
  • a clj-refactor-menu transient replaces the hydra menus, and the multiple-cursors, hydra and inflections dependencies are gone
  • many commands degrade gracefully without a REPL (cljr-clean-ns, cljr-slash, cljr-add-missing-libspec, cljr-remove-let, cljr-promote-function)
  • cljr-slash can add and hotload a missing library, artifact lists are cached, and the namespaced refactor-nrepl ops are used when available

I still think the long-term home for the most useful bits is CIDER and clojure-mode, but at least the project is in good shape while I figure that out. There’s a bit more in the release post.

clj-suitable

clj-suitable, the ClojureScript completion backend, was another project that had been coasting for years:

  • clj-suitable 0.7.0 adapted to Piggieback 0.7’s delegating repl-env, modernized every dependency, replaced the Leiningen build with tools.build, moved CI to GitHub Actions and added a shadow-cljs integration test over a real Node runtime.
  • clj-suitable 0.8.0 brought the static completion much closer to compliment: fuzzy matching (pr-fn completes print-function), compliment-style ranking, completion of local bindings (destructuring included) and of referred vars inside :refer vectors. It also fixed the REPL’s *1/*2/*3 getting clobbered by completions and a few long-standing shadow-cljs and Node.js issues.

ClojureScript users - I’d love to hear how the new completion feels in practice.

Sayid

The Sayid revival continued at a brisk pace, with five releases between July 1 and July 17:

  • Sayid 0.4.0 dropped the com.billpiel namespace prefix, added data-returning variants of the workspace and query ops, and introduced a client-rendered, foldable tree view of the recorded call tree built on CIDER’s cider-tree-view.
  • Sayid 0.5.0 made recording bounded: a record limit, per-function limits, sampling, a max trace depth and bounded printing. Tracing a namespace under a test suite can’t eat all your memory anymore.
  • Sayid 0.6.0 rebuilt inner tracing on tools.analyzer.jvm, replacing the fragile source-rewriting instrumenter.
  • Sayid 0.7.0 added sayid.data (the recorded call tree as plain data, with tap> integration for Portal and friends) and sayid.golden, a golden-trace testing helper.
  • Sayid 0.8.0 focused on the experience: a sayid-menu transient, plain-language feedback from the trace commands, getting-started hints in empty views, and a fix for the inspector integration that had been broken for years.

I wrote a bit more about the last one here. Not bad for a project that was completely dead in June, right?

Drawbridge

Drawbridge is nREPL’s HTTP transport, created by Chas Emerick in 2012 and “technically maintained” ever since. I finally gave it the attention it needed:

  • Drawbridge 0.3.1 updated the dependencies (nREPL 1.7, Ring 1.15) and throttled client polling so it stops flooding servers with GET requests.
  • Drawbridge 0.4.0 is the interesting one. It adds drawbridge.bridge, a local nREPL socket server that relays to a remote Drawbridge endpoint, so any socket-based client (CIDER, Calva, rebel-readline) can now talk to Drawbridge. There’s also a WebSocket transport with server push instead of long-polling, bearer-token authentication via secure-ring-handler (which refuses to run unauthenticated unless you insist), and a deps.edn, so it’s usable as a git dependency.

The full story is in Lowering the Drawbridge.

nREPL

No nREPL release this cycle, but master is shaping up nicely for 1.8:

  • the built-in command-line client can connect using a URL, including the nrepls:// and nrepl+unix: URLs that TLS and filesystem-socket servers advertise, and http(s):// when Drawbridge is on the classpath
  • TLS hardening: descriptive errors for invalid key material, Ed25519 keys, tolerating a swapped certificate order, and a documented security model
  • the built-in client sends input to the server as raw text, so reader typos, auto-resolved keywords and custom tagged literals no longer crash it
  • stdin fixes: EOF arriving behind buffered input is reported properly, and a race between the stdin consumer and producer is gone
  • nrepl.spec finally matches what describe and ls-sessions actually send
  • a pile of documentation debt cleared (lookup return values, the session-closed status, the -f/--repl-fn option, middleware best practices) and a CI check keeping ops.adoc in sync with the descriptors
  • Clojure 1.10 is the new minimum and nrepl.misc/requiring-resolve is gone in favour of the core one

The nrepl.org site also picked up links to several new clients and servers (Nautilos, nREPL.hx for Helix, Janet and Steel Scheme servers). The nREPL family keeps growing, which makes me happy every single time.

nREPL on the BEAM

nrepl-beam is a brand new project I started in July, mostly because I wanted to see how well the nREPL spec holds up when implemented from scratch outside the JVM. It’s home to:

  • dialtone, an nREPL server for Erlang (and a server core for the whole BEAM)
  • repartee, the Elixir server built on top of it
  • chaser, a terminal nREPL client that works with any nREPL server

nrepl-beam 0.1.0 shipped on July 14. Both servers implement the full op set (eval with streamed output, sessions, interrupts, stdin, load-file, completions, lookup) and pass neat’s cross-implementation integration suite alongside Clojure, Babashka and Basilisp. Writing them was a good test of the spec, and it produced a few of the documentation fixes listed above. Turns out that the best way to find holes in a spec is to implement it in a language you barely know.

mezcaml

In the same spirit, mezcaml is a minimal nREPL client for OCaml: a small client library plus a command-line REPL, working against any nREPL server regardless of the language on the other end. No release yet, but the core protocol works, it reads whole forms, and it has server-driven completion. Nothing serious - it was a fun way to combine my recent OCaml hacking with nREPL.

clojure-mode, clojure-ts-mode and MrAnderson

Smaller things: the #_ toggle commands in clojure-mode were renamed to clojure-toggle-discard and friends (matching Clojure’s own terminology, old names kept as aliases), both modes got a :to-have-face matcher for font-lock tests, and clojure-ts-mode now checks the indentation of its sources on CI.

MrAnderson 0.7.1 added a command-line interface, so it can be run without Leiningen, and reworked its downstream integration tests against cider-nrepl and refactor-nrepl, which had silently stopped exercising local changes. Oops.

Blog posts

I wrote a lot this summer, mostly a series on the notable changes in CIDER 2.0:

Epilogue

Big thanks to Clojurists Together, Nubank and the other organizations and people supporting my Clojure OSS work! None of this would have happened without you. You rock!

As for what’s next - CIDER 2.1 is the obvious milestone, and it’s mostly a matter of letting the clojure-ts-mode integration settle. After that I’d like to cut nREPL 1.8 with the TLS and URL work, and get mezcaml and the BEAM servers to a point where they are useful to someone other than me. I won’t make any predictions about productivity this time around. Clearly I’m bad at those.

Keep hacking!

-1:-- Clojurists Together Update: July and August 2026 (Post Meta Redux)--L0--C0--2026-09-10T09:30:00.000Z

Lars Ingebrigtsen: Some LLM spell check experiences

A few days back, I hacked up a little minor mode for Emacs to use an LLM for spell checking. (Before you get a heart attack — I did not say “editing”, “getting ideas” or “research”: Just spell checking. Using an LLM for actual writing isn’t just a bad idea — it leads to abhorrent results.)

I’ve been running a random selection of blog posts I wrote for the Comico read through blog to see whether the thing works well — and… yes! But also no.

OK, positives first:

1) For simple typos, it’s very good indeed — it finds them all.

c) For word substitution errors, like “to” instead of “too”, it’s also very good.

II) For grammar errors, it’s more mixed. It’s good at, for instance, basic noun/verb agreement, so if I’ve written “they writes”, it fixes that. But if the noun clause is long and complex, it doesn’t always react. I’ve experimented with adding/removing an “s” from the verb in sentences like that, and it sometimes lets both versions through without comment.

⁵) Since I’m usually giving the LLM the complete blog post to proof-read, it knows what area I’m writing about, and so it’s able to correct me when I misspell names, too. I hadn’t even considered that, but that was a very nice surprise. So when I’m writing about comics and casually mention “the art looks a bit like Kriegstein”, it knows to correct that to “Krigstein”. This is something that traditional spell check can’t do at all. (And amusingly enough, running this article through the LLM doesn’t tag that “they writes” up there as an error, because it gets that that was an example of an error.)

So that’s all very good. So what’s the bad bits? Well, it’s an LLM, so it’s slow. That’s not a major problem, though, since I’m just using it to give a blog post a look-over to weed out the worst spelling mistakes. But I’ve added some code to strip out (longer) HTML fragments before sending the text over to the LLM, and this helps a bit.

The biggest problem is that an LLM is an LLM and will randomly decide to not follow the instructions in the prompt. Sometimes it adds random elements to the text here and there. Sometimes it says that I should fix a word from “identical” to “identical”. Sometimes it removes a paragraph. This doesn’t happen often — perhaps in 5% of the cases? But…

LLMs are unreliable! It cannot be! *gasp*

(Somebody speculated that this unreliability is a feature for the LLM companies — because you know that you may get bad results, you get extra happy the times you get a good result. The slot machine thing? I think that’s probably the case — some people get really addicted to their LLMs…)

Fortunately, these things can be programmed around — I’ve now added code to do sanity checks to the output from the LLM before using it. So if the LLM decides to go off piste, the package detects that, and then you can just do the query again.

It used to be that doing the same thing and expecting a different result was a sign of insanity, but with an LLM, that’s fine.

It’s a brave new world out there.

It’s also … odd … to type commands in Emacs and then they cost money.

Anyway, I’ve pushed these fixes to Microsoft Github, so there you go.

-1:-- Some LLM spell check experiences (Post Lars Ingebrigtsen)--L0--C0--2026-09-10T08:14:38.000Z

James Cherti: Why Emacs Consult Async Searches Feel slow and How to Speed Them Up? (consult-fd, consult-find, consult-grep, consult-ripgrep...)

The consult Emacs package provides asynchronous search commands such as consult-fd, consult-find, consult-grep, consult-git-grep, and consult-ripgrep. For these commands, Consult does not necessarily start a new search for every change to the minibuffer input. Instead, it uses configurable debounce and throttle delays to control when asynchronous processes start, while a separate refresh delay controls how frequently asynchronous results are pushed to the completion UI.

The first time I tried Consult, I thought to myself: this feels slow compared to Counsel (I had a similar feeling about Emacs settings such as show-paren-delay). With Counsel, results updated immediately on every keystroke, while Consult seemed to hesitate for a fraction of a second before updating the list.

As it turns out, this behavior is entirely intentional. The current Consult defaults are conservative by design.

Aggressive asynchronous search

If you prefer lower latency and speed, these variables can be made more aggressive:

(setq consult-async-input-debounce 0.05
      consult-async-input-throttle 0.1
      consult-async-refresh-delay 0.05)

These values reduce the amount of time Consult waits before starting another asynchronous search and allow the completion UI to refresh much more frequently.

These values do not make the underlying search programs execute faster. External tools like ripgrep already use highly optimized, multi-threaded algorithms to scan files across multiple CPU cores. Instead, these variables speed up the feedback loop inside Emacs itself. They reduce the waiting period before Consult spawns the external process, and causing the Emacs UI to pull in new asynchronous data and redraw the screen at a much faster rate.

Variable: consult-async-input-debounce

(setq consult-async-input-debounce 0.05)

This setting forces Consult to wait 0.05 seconds after your last keystroke before launching an asynchronous process. Because this acts as a debounce delay rather than a fixed polling interval, every new input resets the timer. The search only executes once the full quiet period elapses.

This prevents Emacs from spinning up redundant background tasks for every intermediate character you type, which minimizes CPU overhead and keeps the UI responsive.

Variable: consult-async-input-throttle

(setq consult-async-input-throttle 0.1)

The consult-async-input-throttle variable acts as a hard rate limit for how often Consult starts asynchronous processes. With this value, a new process starts at most once every 0.1 seconds, regardless of how fast you type.

To break down the difference between the two concepts:

  • Debouncing is an idle timer. It waits for you to stop typing. If you keep hitting keys, the timer keeps resetting, and the search will not run until you stop typing.
  • Throttling is a strict speed limit. It enforces a maximum execution rate. Even if you pause just long enough between words to constantly trigger the debounce timer, the throttle ensures the system will not exceed the permitted rate.

Variable: consult-async-refresh-delay

(setq consult-async-refresh-delay 0.05)

The consult-async-refresh-delay variable controls how frequently Consult updates the completion UI with results from asynchronous commands. This value makes Consult refresh at intervals as short as 0.05 seconds when new results require an update. It does not cause Emacs to redraw continuously when there is nothing new to display.

A low refresh delay can make search results appear faster, but redisplay itself has a cost. Emacs redisplay is expensive enough that highly frequent updates can increase CPU and garbage-collection activity.

Choosing values

If you prefer asynchronous searches to feel as responsive as possible, these values offer a good balance between low input latency and resource usage. Consult responds quickly to minibuffer changes and updates the completion UI with minimal delay.

These settings are well suited to fast computers plugged into a power source. On a laptop, the aggressive process creation and continuous UI updates will keep the CPU active and drain your battery much faster than the conservative defaults. On slower computers or when searching very large projects, the default values may provide a better balance by avoiding unnecessary searches while input is still changing.

The optimal values depend on typing speed, project size, search-tool performance, power constraints, and the cost of processing and redisplaying asynchronous results in Emacs. Lowering the delays is therefore best viewed as an explicit trade-off: less input latency in exchange for more frequent asynchronous work.

-1:-- Why Emacs Consult Async Searches Feel slow and How to Speed Them Up? (consult-fd, consult-find, consult-grep, consult-ripgrep...) (Post James Cherti)--L0--C0--2026-09-08T16:54:16.000Z

Irreal: Jinx Update Number …

… I’ve lost track. I’ve been using Jinx for my spelling correction for a while now and really like it. When I first started, my main problem was that the selection procedure described in the documentation didn’t work for me. I was almost certain that was because of an interaction with Ivy; my commenters agreed and offered some things to try.

After some experimentation, I still wasn’t able to get that aspect of Jinx working. I decided to wait for Emacs 31 to revisit the issue. Emacs 31 is here but I still haven’t addressed the problem. Most of the reason for that is that I’ve developed some efficient muscle memory for dealing with the problem. I have jinx-correct bound to Ctrl+; and when I call jinx-correct, I continue holding down Ctrl and can easily move down in the list of alternatives by pressing n. Usually the first choice is the one I want so it’s only a single press. Then I type Return to update the text.

That seems like a little more work than simply pressing a number but I don’t need to move my hands or reach to do it so it seems easy to me. I’ll probably do a bit more research and get the default method working when I get a chance but in the mean time, I’m pretty happy with how it’s working for me now.

I’m really enjoying how easy it is to put an “unknown spelling” in a variety of places—the current file, the current directory, or in a personal global dictionary—and am happy I switched. If you haven’t tried it yet, you should give it a spin.

Update [2026-09-09 Wed 11:55]: Fixed title typo.

-1:-- Jinx Update Number … (Post Irreal)--L0--C0--2026-09-08T14:49:28.000Z

Lars Ingebrigtsen: Calendaring

The other week I made a calendrical display for the eink thing I have. The problem is that I don’t really use a calendar — I guess the main reason is because I hate planning, but the other reason is because I really dislike all the calendar entry interfaces I’ve used.

I haven’t used all that many, though, so I may well be missing some ingenious calendar out there. But what I’ve been resentfully using is the standard Google calendar on Android.

I filmed one of my interactions with this thing, and I swear — I didn’t try to make it more awkward than it is for me:

I’d be climbing up the walls if I were to try to use it more than I do now — I just put the absolute bare necessities in there, like dentist appointments and Boris shows. (One coming up in November!)

But now I have this calendar in the hall… wouldn’t it be nice to have it display more stuff? Stuff that I really should be remembering, anyway?

There’s also org-gcal, if you’re in the Org ecosystem… but… I’m not.

Does a command line interface to Google Calendar work? Because I want to have the calendar in my pocket, too. And… Yes, there’s gcalcli. (Which I noticed hasn’t been updated for two years and is probably going to be deprecated, but it works for now, so I haven’t bothered to redo my code.)

After going through the byzantine process on Google Cloud Console to create an “application”:

Yes! You have to create a logo! Behold my magnificent logo!

Anyway, after all that… tada:

Yes! The system works!

It does the normal Do What I Mean Thing — “fri 9” means “next Friday at 9am”, and “aug 11 1930” means “half past seven on August 11”, etc:

I’m freee! I never have to enter a calendar entry on the mobile phone ever again!

I’ve put the resulting code on Microsoft Github.

-1:-- Calendaring (Post Lars Ingebrigtsen)--L0--C0--2026-09-08T14:48:46.000Z

Sacha Chua: 2026-09-07 Emacs news

The Emacs Carnival theme for September is "Games". Enjoy! Other really cool uses of graphics in Emacs: org-timegrid, poimap, canvas-minimap.

Also, public service announcement: doomemacs.com is not affiliated with doomemacs.org (see this Github discussion). Just in case someone reading this happens to be the person who made doomemacs.com, please consider setting up a redirect to doomemacs.org for more awesomeness and less confusion. Thank you!

And a reminder: EmacsConf - 2026 - Call for Participation target date for proposals Sept 18, conference is online Dec 12-13. Hope to see you there!

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

*

View Org source for this post

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

-1:-- 2026-09-07 Emacs news (Post Sacha Chua)--L0--C0--2026-09-07T23:44:14.000Z

Marcin Borkowski: Pasting primary selection from keyboard

A few years ago I wrote about a setup I coded for myself for when I want to make paying my bills a tiny bit less painful. One thing I mentioned then was the primary selection. For those of you who don’t know it: under X.org, you can select a portion of text (for example by dragging the mouse over it) and then paste it in the same or another application with clicking the middle button. It is extremely useful for quick copy-and-paste operations where you do not have to issue a separate “copy” command (usually C-c, but often also found on mouse context menu). And by the way, Wayland also supports this. Sometimes I find it useful to use it even without the mouse.
-1:-- Pasting primary selection from keyboard (Post Marcin Borkowski)--L0--C0--2026-09-07T19:41:52.000Z

Lars Ingebrigtsen: Emacs Canz Spell Goodly Know

Like many nerds, I feel so embarrassed whenever I see that I’ve misspelled something. But some years back when I started producing text in huge amounts for things like the Eclipse Comics read-through I realised that my options were: 1) Remain neurotic about this stuff and produce one blog post per week, or 2) Start caring less and produce a lot of blog posts.

I think I made the correct decision — I just don’t have time to proff-read all this stuff, so typos will abound. It’s just not feasible to put as much work into that stuff as you’d do for a properly published article. The reader gets what they pay for.

But now there’s a way to have your delicious cake and eat it too — just ask an LLM for help.

NOOOO! *audience runs in slow motion towards camera*

I know, I know. Whenever I read somebody in deep AI psychosis who thinks they’re being AI sceptical saying “well, of course you shouldn’t let the LLM write your articles. Instead I let it do the research, and then I make it provide an outline, and then I write some text based on that, and then it does formats and restates what I wrote. IT”S A WHOLE DIFFERENT THING! I WROTE IT!!!!”

No. You should never let an LLM do those things: Not the research, not the outline, not the formatting, not anything. Reading the output of LLMs is abhorrent to people. You should never subject anybody to an LLM generated text, even if you’ve done “quality control”.

LLM-generated text gives people the ick. It’s better to have an awkwardly phrased article you’ve written yourself than subjecting somebody to a Claude text. It’s not just the ick — it’s the horror.

However, you can use LLMs for spell- and grammer check:

I’ve whipped up a li’l minor mode for Emacs (and put it on Microsoft Github). The default prompt is very clear that I want the LLM to do nothing but copy edits — no structural changes, and no other helpful suggestions. Just fix grammer and tippos.

And then you can interactively go through the fixes and toggle back to the original ones if you disagree.

Yes, yes, sure — Grammarly, etc — stuff like this already exists. But I just don’t like using them. I use the built-in Emacs spellcheck, but it doesn’t catch everything. Having an LLM give the text a quick once-over for obvious mistakes seems nice.

While testing this minor mode now, I see that LLMs haven’t changed as much as I thought — they’ve gotten so much better on programming the last half year, it’s incredible. For stuff like this, though, it’s still pretty unreliable. Any two runs won’t necessarily fix up the same thing, or the same thing the same way. And getting it (I’m using Claude Sonnet 5) to output it in the requested form is not reliable at all — I’ve been reformulating the prompt, trying to get it to output it in the format I the mode needs, and… I think I’m at 95% now?

So, like all with all non-programming tasks, LLMs are still pretty much toys. But useful ones! For instance, this blog post is totally free of errors.

-1:-- Emacs Canz Spell Goodly Know (Post Lars Ingebrigtsen)--L0--C0--2026-09-07T15:41:27.000Z

Magnus: Emacs salmagundi, 2026-09-06

Compilation commands by project type

I while ago I added a minor mode, haskell-ng-project-mode, to my Haskell mode. The idea I had was that I could use it to add keybindings for running various tools (via compile), e.g. I bound cabal build and cabal test , p b and , p t respectively. Then I hooked in the minor mode in my haskell-ng-mode. Soon I realised it'd be nice to have those shortcuts available in dired too, so I hooked it in there as well. When I later wanted to re-run the tests after modifying a JSON file that was part of a golden test I realised that this wasn't a very nice solution at all.

After looking around a bit I found emacs-multi-compile. I removed the minor mode and add this configuration instead

(add-to-list 'multi-compile-alist
             '((haskell-ng-is-project) . (("cabal build" "cabal build -j --semaphore" (project-root (project-current)))
                                          ("cabal test" "cabal test" (project-root (project-current)))
                                          ("fourmolu" "fourmolu -i $(fd .hs$)" (project-root (project-current))))))

I've had some vague thoughts that it'd be rather easy to write something slightly more custom, but emacs-multi-compile works very well for me so I'm very happy with it.

Jumping between implementation and test, again

I wrote about this a while ago. At the time I came up with a way of bending ff-find-other-file to my will, but soon after that I ran into a case where there may be 2 "other files" – one of the Haskell projects at work put unit tests in files ending in Spec.hs and property tests in filed ending with Prop.hs. Unfortunately this is a situation that ff-find-other-file can't handle.

I ended up hacking together consult-kith to deal with it. The configuration to handle two kinds of test files looks like this

(setq-local consult-kith-alist `((,(rx (seq "Spec.hs" eol)) (".hs"))
                                 (,(rx (seq "Prop.hs" eol)) (".hs"))
                                 (,(rx (seq ".hs" eol)) ("Prop.hs" "Spec.hs")))
            consult-kith-search-directories '("src" "test"))

The project has since settled on only using Spec.hs for tests, but I'm sticking to consult-kith for now.

-1:-- Emacs salmagundi, 2026-09-06 (Post Magnus)--L0--C0--2026-09-06T16:33:00.000Z

Irreal: Prot’s Doric Themes

As many of you know, my idea of a theme is to set the background to “oldlace” and the cursor to red. For the rest of it, I just take what Emacs gives me. That sounds pretty haphazard but it works out pretty well. It has enough contrast to be easy to read without searing my eyeballs and has enough syntax highlighting to be useful without being overpowering.

Prot (Protesilaos Stavrou) has a much more refined sense for these things. He considers what colors go well to together and things like contrast levels. Just the oposite of my “do with me what you will” approach. Nevertheless, I mostly don’t bother much with formally defined themes although I do occasionally like to take a look at them.

The reason for this post is Prot’s latest release of his Doric Themes. They’re sort of minimalist, like my non-theme theme but still very nice looking if you don’t want your Emacs screen to look like an arcade. The doric-almond variation is, in particular, very similar to my color scheme. Mine has a little more contrast: the foreground text is a bit darker and the keywords in my code buffers are a blue that’s slightly brighter than doric-almond’s light green. Still, it’s a theme that I’d be comfortable with if I hadn’t taken the easy way.

Some people have multiple themes and change them according to the time of day, their mood, the phase of the moon, or whatever. I’d hate that probably for the same reason that I don’t do well with trying to use different editors: I’d always be confused as to what’s what.

If you’re looking for a minimalist theme and my method seems too minimal, take a look at Prot’s Doric Themes. There are several variations in both light and dark mode.

-1:-- Prot’s Doric Themes (Post Irreal)--L0--C0--2026-09-06T14:48:40.000Z

James Dyer: Git Worktrees Without Leaving Built-in VC

More vc stuff, and this week's rabbit hole was git worktrees (Bozhidar Batsov's Working with Git Worktrees in Magit is what sent me down this particular rabbit hole!). It was an itch that manifested itself while reading this blog and mainly seeing that worktrees seemed to make a good deal of sense to me (especially as I am quite a subversion veteran). I have always felt uncomfortable with the default git in-situ branch switching and prefer to branch develop away from the mainline in a physical separate directory and this is just what worktrees facilitate. So how to do this with built-in vc-mode tools?, well as always this requires a little work and yes, I could use Magit, but I'm seeing how far I can push the built in vc.

20260905090959-emacs--Git-Worktrees-and-Branch-Surgery-in-Built-in-VC.jpg

The funny thing is that vc already understands worktrees perfectly well: a worktree is just a directory holding a .git pointer file instead of a .git folder, vc-git resolves that transparently, and diffs, commits and vc-dir all quietly do the right thing per worktree. What vc cannot do is actually seemingly manage them. There is no command to create one, switch between them, move one, list them, or throw one away, and the branch side is nearly as thin. Magit does all of this beautifully, of course - but I am stubborn about staying inside core Emacs where I can, so I spent an evening teaching vc-dir some new tricks. And yes, I do understand that vc in Emacs is in fact a generic abstraction and works well across all different types of version control, hence the reduction to a core subset of general source code commands, with a few extra added on of course for those obvious missing pieces, but I want to add the less obvious pieces, and certainly for git, worktrees is probably going to be one of them. Right lets get on with this and write some code!

Everything funnels through one small worker that shells out via vc-git-command and then drops you straight into vc-dir in the new tree:

Here is what I implemented:

;;
;; -> vc-git-worktree-core
;;
(defun my/vc-git-branches ()
  "Return a list of local branch names in the current git repository."
  (let ((root (vc-git-root default-directory)))
    (when root
      (with-temp-buffer
        (let ((default-directory root))
          (vc-git-command (current-buffer) 0 nil
                          "branch" "--format=%(refname:short)"))
        (split-string (buffer-string) "\n" t)))))

(defun my/vc-git--worktree-root ()
  "Return the git worktree root for `default-directory'.
Signal a `user-error' when not inside a Git repository."
  (or (vc-git-root default-directory)
      (user-error "Not in a Git repository")))

(defun my/vc-git-worktree-entries ()
  "Return the worktrees of the current repository as a list of plists.
Each plist has keys :path, :head, :branch, :detached, :bare,
:locked and :prunable, parsed from `git worktree list --porcelain'."
  (let ((root (my/vc-git--worktree-root))
        entries current)
    (with-temp-buffer
      (let ((default-directory root))
        (vc-git-command (current-buffer) 0 nil
                        "worktree" "list" "--porcelain"))
      (goto-char (point-min))
      (while (not (eobp))
        (let ((line (buffer-substring-no-properties
                     (line-beginning-position) (line-end-position))))
          (cond
           ((string-prefix-p "worktree " line)
            (when current (push current entries))
            (setq current (list :path (substring line 9))))
           ((string-prefix-p "HEAD " line)
            (setq current (plist-put current :head (substring line 5))))
           ((string-prefix-p "branch " line)
            (let ((ref (substring line 7)))
              (setq current
                    (plist-put current :branch
                               (if (string-prefix-p "refs/heads/" ref)
                                   (substring ref 11)
                                 ref)))))
           ((string= line "detached")
            (setq current (plist-put current :detached t)))
           ((string= line "bare")
            (setq current (plist-put current :bare t)))
           ((string-prefix-p "locked" line)
            (setq current
                  (plist-put current :locked
                               (if (> (length line) 6)
                                   (substring line 7)
                                 t))))
           ((string-prefix-p "prunable" line)
            (setq current
                  (plist-put current :prunable
                               (if (> (length line) 8)
                                   (substring line 9)
                                 t))))))
        (forward-line 1)))
    (when current (push current entries))
    (nreverse entries)))

(defun my/vc-git--default-worktree-path (root branch)
  "Return a default new-worktree path for BRANCH next to ROOT.
The default is a sibling directory of ROOT prefixed with the project
name, e.g. <parent>/myproj-feature for branch feature of myproj.
Slashes in BRANCH become dashes so hierarchical branches cannot
collide; an empty BRANCH falls back to <project>-worktree."
  (let ((project (file-name-nondirectory
                  (directory-file-name (expand-file-name root)))))
    (expand-file-name
     (concat project "-"
             (if (string-empty-p branch)
                 "worktree"
               (replace-regexp-in-string "/" "-" branch)))
     (file-name-directory (directory-file-name (expand-file-name root))))))

(defun my/vc-git-worktree-add (path branch &optional new-branch start-point)
  "Create a Git worktree at PATH checking out BRANCH, then open `vc-dir'.
An empty BRANCH checks out HEAD.  With prefix argument NEW-BRANCH,
create BRANCH as a new branch from START-POINT (defaulting to HEAD)
instead of checking out an existing branch or revision."
  (interactive
   (let* ((root (my/vc-git--worktree-root))
          (branches (or (my/vc-git-branches) '()))
          (make-new (if current-prefix-arg t nil))
          (branch (if make-new
                      (read-string "New branch name: ")
                    (completing-read "Checkout branch (empty = HEAD): "
                                     branches nil nil)))
          (default-path (my/vc-git--default-worktree-path root branch))
          (path (read-file-name "Worktree location: "
                                (file-name-directory default-path)
                                nil nil
                                (file-name-nondirectory default-path)))
          (start (and make-new
                      (completing-read "Start point: "
                                       (delete-dups
                                        (append (list "HEAD") branches
                                                (my/vc-git-tags)))
                                       nil nil nil nil "HEAD"))))
     (list (expand-file-name path) branch make-new start)))
  (my/vc-git-worktree--create path branch new-branch start-point))

(defun my/vc-git-worktree--create (path branch new-branch start-point)
  "Create a Git worktree at PATH and open `vc-dir' there.
BRANCH names an existing branch or revision, unless NEW-BRANCH is
non-nil, in which case BRANCH is created from START-POINT first."
  (let ((root (my/vc-git--worktree-root))
        (expanded (expand-file-name path)))
    (let ((default-directory root))
      (cond
       (new-branch
        (when (string-empty-p branch)
          (user-error "New branch name must not be empty"))
        (vc-git-command nil 0 nil "worktree" "add" "-b" branch expanded
                        (if (string-empty-p start-point) "HEAD" start-point)))
       ((string-empty-p branch)
        (vc-git-command nil 0 nil "worktree" "add" expanded))
       (t
        (vc-git-command nil 0 nil "worktree" "add" expanded branch))))
    (message "Created worktree %s" expanded)
    (vc-dir expanded)))

(defun my/vc-git-worktree-checkout (path branch)
  "Check out existing BRANCH in a new worktree at PATH, then open `vc-dir'.
The Magit-style `magit-worktree-checkout' first action: the location
defaults to a sibling directory named <project>-<branch>."
  (interactive
   (let* ((root (my/vc-git--worktree-root))
          (branches (or (my/vc-git-branches) '()))
          (branch (completing-read "Checkout branch in new worktree: "
                                   branches nil nil))
          (default-path (my/vc-git--default-worktree-path root branch))
          (path (read-file-name "Worktree location: "
                                (file-name-directory default-path)
                                nil nil
                                (file-name-nondirectory default-path))))
     (list (expand-file-name path) branch)))
  (when (string-empty-p branch)
    (user-error "Branch must not be empty"))
  (my/vc-git-worktree--create path branch nil nil))

(defun my/vc-git-worktree-branch (path branch start-point)
  "Create new BRANCH from START-POINT in a new worktree at PATH.
Then open `vc-dir'.  The Magit-style `magit-worktree-branch' first
action: the location defaults to a sibling directory named <project>-<branch>."
  (interactive
   (let* ((root (my/vc-git--worktree-root))
          (branches (or (my/vc-git-branches) '()))
          (branch (read-string "New branch name: "))
          (start (completing-read "Start point: "
                                  (delete-dups
                                   (append (list "HEAD") branches
                                           (my/vc-git-tags)))
                                  nil nil nil nil "HEAD"))
          (default-path (my/vc-git--default-worktree-path root branch))
          (path (read-file-name "Worktree location: "
                                (file-name-directory default-path)
                                nil nil
                                (file-name-nondirectory default-path))))
     (list (expand-file-name path) branch start)))
  (my/vc-git-worktree--create path branch t start-point))

(defun my/vc-git-worktree-move (path new-path)
  "Move the Git worktree at PATH to NEW-PATH (`git worktree move').
Interactively, prompt for a worktree, defaulting to the current one."
  (interactive
   (let* ((entries (my/vc-git-worktree-entries))
          (paths (mapcar (lambda (e) (plist-get e :path)) entries))
          (current (my/vc-git--worktree-root)))
     (unless paths (user-error "No Git worktrees found"))
     (let ((path (completing-read "Move worktree: " paths
                                  nil t nil nil current)))
       (list path (read-file-name
                   "Move to: "
                   (file-name-directory
                    (directory-file-name (expand-file-name path)))
                   nil nil)))))
  (let* ((root (my/vc-git--worktree-root))
         (expanded (expand-file-name path))
         (target (expand-file-name new-path)))
    (let ((default-directory root))
      (vc-git-command nil 0 nil "worktree" "move" expanded target))
    (message "Moved worktree %s to %s" expanded target)
    (when (derived-mode-p 'vc-dir-mode) (vc-dir-refresh))))

(defun my/vc-git-worktree-switch (path)
  "Interactively select an existing Git worktree and open `vc-dir' in it."
  (interactive
   (let* ((entries (my/vc-git-worktree-entries))
          (table (mapcar (lambda (e)
                           (cons (plist-get e :path) e))
                         entries))
          (current (my/vc-git--worktree-root)))
     (unless table (user-error "No Git worktrees found"))
     (let ((completion-extra-properties
            (list :annotation-function
                  (lambda (cand)
                    (let ((e (cdr (assoc cand table))))
                      (when e
                        (concat "  ["
                                (or (plist-get e :branch)
                                    (and (plist-get e :detached)
                                         "detached")
                                    "?")
                                (when (plist-get e :bare) ", bare")
                                "]")))))))
       (list (completing-read "Switch to worktree: " table
                              nil t nil nil current)))))
  (vc-dir path))

(defun my/vc-git-worktree-remove (path &optional force)
  "Remove the Git worktree at PATH, prompting first for confirmation.
With prefix argument FORCE, pass --force to git.
Interactively, prompt for a worktree, defaulting to the current one."
  (interactive
   (let* ((entries (my/vc-git-worktree-entries))
          (paths (mapcar (lambda (e) (plist-get e :path)) entries))
          (current (my/vc-git--worktree-root)))
     (unless paths (user-error "No Git worktrees found"))
     (list (completing-read "Remove worktree: " paths
                            nil t nil nil current)
           (if current-prefix-arg t nil))))
  (let* ((root (my/vc-git--worktree-root))
         (expanded (expand-file-name path))
         (main-root (plist-get (car (my/vc-git-worktree-entries)) :path))
         (here (file-truename default-directory))
         (gone (file-truename expanded)))
    (when (yes-or-no-p (format "Remove worktree %s%s? "
                               expanded (if force " (forced)" "")))
      (let ((default-directory root))
        (if force
            (vc-git-command nil 0 nil "worktree" "remove" "--force" expanded)
          (vc-git-command nil 0 nil "worktree" "remove" expanded)))
      (message "Removed worktree %s" expanded)
      (when (derived-mode-p 'vc-dir-mode)
        (if (string-prefix-p gone here)
            (vc-dir (or main-root root))
          (vc-dir-refresh))))))

(defun my/vc-git-worktree-prune ()
  "Prune stale Git worktree metadata (`git worktree prune')."
  (interactive)
  (let ((root (my/vc-git--worktree-root)))
    (let ((default-directory root))
      (vc-git-command nil 0 nil "worktree" "prune"))
    (message "Pruned worktrees in %s" root)
    (when (derived-mode-p 'vc-dir-mode) (vc-dir-refresh))))

(defvar-local my/vc-git-worktree-list--root nil
  "Repository root shown in the current worktree list buffer.")

(defvar my/vc-git-worktree-list-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "RET") #'my/vc-git-worktree-list-visit)
    (define-key map (kbd "g") #'my/vc-git-worktree-list)
    (define-key map (kbd "q") #'quit-window)
    map)
  "Keymap for `my/vc-git-worktree-list-mode'.")

(define-derived-mode my/vc-git-worktree-list-mode special-mode "Worktrees"
  "Major mode for listing Git worktrees.
\\{my/vc-git-worktree-list-mode-map}")

(defun my/vc-git-worktree-list-visit ()
  "Open `vc-dir' for the worktree on the current line."
  (interactive)
  (let* ((here (line-number-at-pos))
         (btn (or (button-at (point))
                  (save-excursion
                    (beginning-of-line)
                    (next-button (point) t))))
         (ok (and btn (= here (line-number-at-pos (button-start btn))))))
    (if ok
        (vc-dir (button-label btn))
      (user-error "No worktree on this line"))))

(defun my/vc-git-worktree-list ()
  "List Git worktrees of the current repository in a dedicated buffer.
RET on a path opens `vc-dir' there; `g' refreshes the list, `q' quits."
  (interactive)
  (let* ((root (if (derived-mode-p 'my/vc-git-worktree-list-mode)
                   (or my/vc-git-worktree-list--root
                       (my/vc-git--worktree-root))
                 (my/vc-git--worktree-root)))
         (default-directory root)
         (entries (my/vc-git-worktree-entries)))
    (unless entries (user-error "No Git worktrees found"))
    (with-current-buffer (get-buffer-create "*vc-git worktrees*")
      (let ((inhibit-read-only t))
        (erase-buffer)
        (my/vc-git-worktree-list-mode)
        (setq my/vc-git-worktree-list--root root)
        (setq default-directory root)
        (insert (format "Worktrees for %s\n\n" root))
        (dolist (e entries)
          (let* ((path (plist-get e :path))
                 (head (or (plist-get e :head) ""))
                 (short (if (> (length head) 7) (substring head 0 7) head))
                 (flags (string-join
                         (delq nil
                               (list (when (plist-get e :bare) "bare")
                                     (when (plist-get e :detached) "detached")
                                     (when (plist-get e :locked) "locked")
                                     (when (plist-get e :prunable)
                                       "prunable")))
                         ",")))
            (insert-button path
                           'action (lambda (btn)
                                      (vc-dir (button-label btn)))
                           'follow-link t)
            (insert (format "  [%s] %s%s\n"
                            (or (plist-get e :branch) "HEAD")
                            short
                            (if (string-empty-p flags)
                                ""
                              (concat " (" flags ")")))))))
      (goto-char (point-min))
      (pop-to-buffer (current-buffer)))))

(with-eval-after-load 'vc-dir
  (define-key vc-dir-mode-map (kbd "Z a") #'my/vc-git-worktree-add)
  (define-key vc-dir-mode-map (kbd "Z b") #'my/vc-git-worktree-checkout)
  (define-key vc-dir-mode-map (kbd "Z c") #'my/vc-git-worktree-branch)
  (define-key vc-dir-mode-map (kbd "Z g") #'my/vc-git-worktree-switch)
  (define-key vc-dir-mode-map (kbd "Z k") #'my/vc-git-worktree-remove)
  (define-key vc-dir-mode-map (kbd "Z l") #'my/vc-git-worktree-list)
  (define-key vc-dir-mode-map (kbd "Z m") #'my/vc-git-worktree-move)
  (define-key vc-dir-mode-map (kbd "Z p") #'my/vc-git-worktree-prune))

Some interactive commands: Z a for the general case, Z b for checking out an existing branch, Z c for a new branch from a start point, plus Z m to move a tree, Z g to switch between them with branch annotations in the completion, Z k to remove one (prefix forces, and a vc-dir sitting inside the removed tree falls back to the main root rather than dying), Z p to prune stale metadata, and Z l for a listing buffer where RET jumps to vc-dir. The letters deliberately mirror Magit's own worktree map, so Z b, Z c, Z g, Z m and Z k all do what a Magit user's fingers expect; Z a, Z p and Z l are extras with no Magit equivalent. The same set hangs off C-x v z … globally.

So the whole loop, start to finish, never leaves vc: Z c to spin up the worktree, hack away, C-x v v to commit, Z g back to main, merge the branch, and Z k to remove the worktree.

If any of this sounds useful, the pattern to steal is a small one: vc-git-command with default-directory bound to the worktree root does almost all of the heavy lifting, and vc-dir on the resulting path gives you the status buffer for free. The rest is just prompts and keybindings.

-1:-- Git Worktrees Without Leaving Built-in VC (Post James Dyer)--L0--C0--2026-09-06T08:30:00.000Z

Joar von Arndt: Emacs does not need LSP


Language-server-protocol (lsp) is standard to communicate information about a current programming project to an editor integration, powered by a “language server” for the specific programming language. Emacs has two major implementations of this editor functionality: eglot (built-in) and lsp-mode. Getting this set up is usually one of the first things that new users will want to do, but in my experience one can get by for a surprisingly long time using Emacs without needing to reach for eglot or lsp-mode. That is because many of the functions that lsp-servers provide can be provided by Emacs through other means.

As someone who quite often moves between different programming languages, I find the need to set up each lsp-server to a bit clunky. Despite being presented as a simple one-stop solution, you often need to research exactly what server to use, how to set it up, and to tailor it to your machine in some ways. For me, this only makes sense in a few languages that I use often, and even then it might not be the first thing I take care of.

In-buffer completion

The most obvious functionality is auto-complete (aka in-buffer completion). In Emacs this is provided on multiple levels, which is sometimes confusing for new users. There are many ways to show in-buffer completions, but they are all powered by the same back-end functionality: completion-at-point-functions (capf). This is a list of functions that run in order, only returning a value if there is something to complete.

Some major modes (most notable perhaps lisp-interaction-mode) come with their own buffer-local values of capf1 that provide buffer-specific completions. completion-at-point will then move on to the global value if the buffer-local value of capf includes the value t. It is therefore this global value that we are often interested in modifying.

For this we can use the cape-package — standing for “Completion At Point Functions”. Specifically, we can make liberal use of the cape-dabbrev-function. This capf makes use of Emacs’ built-in dabbrev-functionality. Dabbrev will scan all the words in the buffers of your choice and give you completions for words that you have already used. This means that, while working on your project, you have quick and easy access to all the keywords and function names in your currently opened buffers.

This also works as a form of autocomplete for writing prose, and so we can combine these two using cape-wrap-super:

(defun cape-language (&optional interactive)
    (interactive (list t))
    (if interactive
        (cape-interactive #'cape-language)
      (cape-wrap-super #'cape-dabbrev #'cape-dict)))

This will combine the output cape-dabbrev and cape-dict2 into one capf that can run at the same time (and therefore not block each other). For a simpler contribution we also use the cape-keyword capf that comes with a number of pre-configured programming language3 keywords. This is a bit of a pre-lsp solution, where each editor provided support for each programming language. To set up cape, simple add something like this to your init file:

(add-hook 'completion-at-point-functions #'cape-history)
(add-hook 'completion-at-point-functions #'cape-keyword)
(add-hook 'completion-at-point-functions #'cape-file)
(add-hook 'completion-at-point-functions #'cape-language)
(add-hook 'completion-at-point-functions #'cape-abbrev)

This is the order in which cape will try your capfs, with the first matching blocking the rest. As mentioned earlier, locally bound capfs will supersede these global values, and so python-completion-at-point will run before any of these in python-mode buffers.

Another improvement we can do is to change the value of cape-dabbrev-buffer-function. The value of this variable is the function that returns the buffers to scan for dabbrev results. By default this is cape-same-mode-buffers, meaning that only buffers in the same mode (org-mode, python-mode, et cetera) will be used for results. This is a good default, but I would rather have always have some relevant completion candidate than nothing. So personally I set this value to cape-text-buffers, which will return all buffers in text-mode or prog-mode buffers. This means extra niceties like referring to practically anything you are working on when writing a commit message in git.

Jump to definition and references

A second major feature of lsp-servers is “jump to definition” that allows you to go to the code that creates a function (its definition) to inspect its inner workings. There are a few different ways to do this.

The first is the package named dumb-jump that uses ag, rg, or simply grep to find definition-matching strings. This works similarly to cape-keyword in that it uses a pre-configured set of language-syntax elements to match function-defining cases.4 I however do not personally use this.

Instead I use another of Minad’s wonderful additions to the Emacs’ ecosystem; consult. I must confess that I slept on consult for far too long. I had it in my config for a long time, but the number of (largely stand-alone) functions made it a bit overwhelming to learn.5 What Consult offers is a suite of improved versions of preëxisting Emacs utilities (and a few new ones building upon said features), largely based on the completing-read.

Emacs has a built-in feature called imenu that scans the buffer’s structure for top-level features (Org-mode and markdown headers, or in this case function definitions) and allows you to quickly move to them. Consult of course expands on this by instantly showing you the place you will move to, but it also offers an extension of this feature: consult-imenu-multi (as opposed to the regular consult-imenu). This will not only scan the current buffer, but also all other same-mode buffers. Better yet, it integrates with Emacs’ project.el, and so only checks buffers that belong to the same project (exempli gratia git repository). As I usually have the relevant buffers open in a project I am working on, this works very well as an alternative to dumb-jump, and the preview and subsequent confirmation mean that I am not worried about jumping to the wrong place — although of course at the cost of speed.

Then we of course have the venerable xref built into Emacs. This works a bit differently depending on the programming language backend, and plugs into an lsp-server automatically if it is available. Use xref-find-references6 Otherwise we can use etags-regen to automatically create and refresh the TAGS file that keeps track of the project structure:

(use-package etags-regen
  :config
  (setq etags-regen-ignores
        '("*.pyc" ".git" ".venv" "venv" "node_modules"))
  (etags-regen-mode 1))

Consult can also be used to improve upon this as well by replacing the ui interaction component:

(setq xref-show-xrefs-function #'consult-xref
      xref-show-definitions-function #'consult-xref)

The reason why I usually prefer using imenu is because it requires no trip to inspect the state of any files on the system — everything is already loaded and available, and perhaps 90% of the time it includes the things that I need (since they are things I am working on).

Syntax checking

Here we can use the built-in flymake or the standalone flycheck. Both of these make use of external syntax-checking tools for each language, and so do require some more setup on the host machine than the above solutions. In fact, Flycheck can even act as its own lsp-server just for syntax checking. But my experience has still been that this is easier to deal with than lsp-servers that need to be configured, started, reconnected, that crash, or have some other issue that need to be dealt with; A simple unix-style shell command that prints text (and that is then processed by Flymake or Flycheck) rarely needs much setup or maintenance in my experience. This is a matter that I really just want out of my mind.

Conclusion

There are only so many things that can be done merely through these static solutions. For example, neither of our in-buffer completion or jump-to-definition solution will complete external libraries; only buffers that have already been opened. There is no hover information.

I am not ideologically opposed to the use of language servers, and I will sometimes intentionally start one if I expect to be working on a larger code base for a longer amount of time. But tools like the ones above are nevertheless incredibly useful because they work in all buffers and contexts.

One of the strengths of Emacs is how it acts as a sort of force-multiplier for any task interacting with text, and how it builds upon improvements in one area throughout the entire editor. Having some of the most critical lsp-like abilities available anywhere you want without any extra effort is incredibly powerful. This is especially noteworthy for things like cape-dabbrev since it has made me so spoiled for autocompletion for everything I write.

I am sure that there are lots of different solutions and tools that I have missed that would work to replace even more lsp-based ones, and probably solutions that work better and/or are more elegant than the ones I use — the answer may even be to just use lsp. But honestly I only think about the matter of language server when I am talking with others. In day to day life I rarely feel the need for them at all. ❦

Footnotes:

1

For lisp-interaction-mode the value is (elisp-completion-at-point t).

2

The results of cape-dict are decided by the value of the cape-dict-file variable.

3

Here is the list of programming languages supported:

C++, C, Caml, Crystal, C#, D, Elixir, Erlang, f90, Go, Java, Javascript, Kotlin, Lua, Nim. Objective C, Perl, php, Purescript, Python, Ruby, Rust, Scala, Scheme, Swift, Julia, Thrift, sh.

4

This supposedly misidentifies things sometimes; as expected, it is “dumb” after all.

5

If you are using the built-in switch-to-buffer command (by default bound to C-x b) I highly recommend switching it to consult-buffer instead. Instant “previews” is super useful, and I often just spam C-n (N being right next to B) to cycle through my open buffers instead of starting to write the exact name that I want.

6

By default bound to M-?. To run xref-go-back press M-,.

-1:-- Emacs does not need LSP (Post Joar von Arndt)--L0--C0--2026-09-05T16:00:00.000Z

Irreal: Optimizing Startup With Use-package

James Cherti has another excellent post, this time on optimizing Emacs startup with use-package. The idea is that many configurations load a substantial number of packages and that this can cause a significant increase in startup time.

Most of you know by now that we here at the Irreal bunker refuse to worry about that because, after all, we hardly ever restart Emacs. Still, plenty of people do worry about it and there’s nothing wrong with learning a few easy to implement principles that will speedup Emacs startup without having to obsess about tiny details.

The TL;DR is to get use-package to defer loading of packages until they are actually needed. Most Emacers who aren’t complete n00bs already know this. The question is, how exactly, to specify the use-package commands to achieve that.

The obvious answers aren’t the right ones. For example, the :defer keyword seems like the natural answer but Cherti explains why this isn’t usually necessary and that it doesn’t do what you probably think it does. It’s much better to use one of the commands that implicitly defer loading but also arrange for autoloading the required functions when they’re needed.

Cherti also explains how to get use-package to add logging information to the *Messages* buffer to help with understanding the load process. There’s a lot of information in Cherti’s post and it serves as a handy reference for how to use use-package to control the loading of packages at startup time. If you’re at all worried about startup time or just want to understand how to use use-package to control package loading, take a look at his post.

-1:-- Optimizing Startup With Use-package (Post Irreal)--L0--C0--2026-09-05T14:44:09.000Z

Sacha Chua: Emacs Carnival September 2026: Games

The theme for the Emacs Carnival this September is "Games". Thanks to Raymond Zeitler for hosting!

One of the things I love about the Emacs community is people's sense of humour and whimsy. There's a grand tradition of people hiding Easter eggs in software, a bit of hidden fun that might make someone smile. But isn't this just bloat and frivolity? One might argue that the code that lets us play M-x tetris, dunnet, or doctor in Emacs is a waste of time and space compared to More Serious Things, but on the other hand, the Tetris source code could be a good jumping-off point if you find yourself wanting to make an interactive real-time simulation that displays graphical results. Want something more modern? Maybe DOOM on Emacs! Besides, the code for games isn't actually loaded until you choose to play it. It's okay for something to be fun by itself instead of needing to be productive. There's something wonderful about a tool that people choose not just for work but also for enjoyment.

Our adventure can start with Fun and Games in Emacs - Mastering Emacs or Emacs Is A Gaming Platform for Windows, Mac and Linux - YouTube, which cover a lot of classic built-ins. Here's a barrage of ideas; skip or explore anything you want: Pretend you're on a typewriter or turn on power mode or make c-c-combos. Go on to draw some ASCII art with artist-mode or with Emacs Lisp, and add some box shadows because why not. Maybe even use it to tell the time. Turn ASCII art into PNGs. Have fun with obscure calendars. Use kaomojis or write weird text. Explore the screensavers in zone and let them sprinkle characters through your buffer, take over your other frames, or teach you something new. Watch snow or a fireplace. Rotate cubes in ASCII or with hardware-accelerated graphics. Why limit yourself to cubes? Still, if you do want to stick to cubes, you can solve them. Go ahead, animate something. Play roleplaying games (also check out Howard Abrams's 2023 talk), rogue-likes, multi-user dungeons, classics, chess, trading, Klondike and lots of other card games, a racing game, Minesweeper, 2048, Boomshine, a Gameboy emulator (this other one handles NES). Simulate cities or physics or pets (here's another one) or life. Have fun with so many cats: timers, scrollcats, screensavers. Not a cat person? There's a parrot version and a fish version. Read comics or make memes. Guess words or find them. Learn other alphabets. Make art. You can even turn your Emacs into a Geiger counter for garbage collection.

Then take a step back and marvel at this community of people who try things out, tinker, follow that crazy idea that won't let go of them, and share what they came up with. If you write Emacs Lisp (or want to learn), rummage around in the source code for ideas on how to do things. See if you have any ideas that tickle your fancy. You might be able to get Emacs to do things text editors don't usually do! =)

Check out the Emacs Carnival page for Games to see other posts!

View Org source for this post

You can view 1 comment or e-mail me at sacha@sachachua.com.

-1:-- Emacs Carnival September 2026: Games (Post Sacha Chua)--L0--C0--2026-09-04T01:04:46.000Z

Protesilaos: Emacs: doric-themes version 1.3.0

These are my minimalist themes. They use few colours and will appear mostly monochromatic in many contexts. Styles involve the careful use of typography, such as italics and bold italics.

If you want maximalist themes in terms of colour, check my ef-themes package. For something in-between, which I would consider the best “default theme” for a text editor, opt for my modus-themes.

Below are the release notes.


Version 1.3.0 on 2026-09-04

New theme screenshots

All sample images are updated to the current version and cover all the Doric themes: https://protesilaos.com/emacs/doric-themes-pictures.

New doric-lilac and doric-borage themes

I drew inspiration for both themes from plants that exist around my house. They both use the same colours in different shades and sequence. doric-lilac has a light background while doric-borage has a dark background.

More commands to load a theme

The commands doric-themes-rotate-light and doric-themes-rotate-dark are convenience wrappers of doric-themes-rotate. As their name suggests, they are restricted to one kind of theme in the collection.

Similarly, doric-themes-load-random-light and doric-themes-load-random-dark are variants of doric-themes-load-random.

Thanks to Lucas Jiménez for making this suggestion in issue 28: https://github.com/protesilaos/doric-themes/issues/28.

Broader face coverage

The following packages have some new faces or I tweaked them to look better with the rest of the theme.

  • completion-preview
  • elfeed
  • erc
  • flymake
  • gnus
  • mu4e
  • query-replace

Minor refinements to palette values

I made several small tweaks to several themes. The changes are probably too fine to notice or they should now look correct enough to be unnoticeable.

-1:-- Emacs: doric-themes version 1.3.0 (Post Protesilaos)--L0--C0--2026-09-04T00:00:00.000Z

James Cherti: Optimizing Emacs Startup - Guide to Deferred Package Loading with use-package

As an Emacs user, your configuration can easily grow from a few lightweight adjustments to a massive, hundred-package IDE. Without careful management, Emacs startup time can degrade from sub-second execution to several seconds, or minutes, in the worst cases. Eager package loading is one common source of startup overhead. This guide explains how Emacs loads libraries, how use-package configures package loading, and how deferred loading can reduce startup time.

Before we dive in, please consider sharing this article on your website, blog, Mastodon, Reddit, X, Linkedin, or other social media platforms.

Why does Emacs startup get slow?

A naive package declaration looks like this:

;; The kirigami Emacs package provides a unified method to fold and unfold
;; text in Emacs across a diverse set of Emacs modes.
(use-package kirigami)

By default, this causes use-package to load the package during initialization, unless the declaration contains a deferring keyword or another option that changes the loading behavior.

Conceptually, the eager loading is equivalent to:

(require 'kirigami)

The require function ensures that a feature is loaded.

When many packages are loaded this way, the time spent loading and evaluating their code contributes to increasing startup time.

What is Autoloading?

Autoloading registers a function without immediately loading the library that defines it. The library is loaded only when the function is called. For example:

;; Simplified representation of an autoload registration
(autoload 'kirigami-global-mode "kirigami" "Global mode for Kirigami." t)

If M-x kirigami-global-mode invokes the autoload, Emacs loads the kirigami.el library ("kirigami", the second argument), and then calls the actual kirigami-global-mode function. This allows the library's startup cost to be deferred until the function is actually needed.

Explicit vs. implicit deferral in use-package

The use-package macro makes package configuration and loading declarative. By default, a declaration is eager, meaning the package is loaded immediately while Emacs processes the configuration during startup, rather than waiting until the package is needed. A deferring keyword or another loading mechanism can postpone loading until later.

The explicit :defer keyword

Deferral using :defer t (Not necessary)

Note: Explicitly using :defer t is often unnecessary. Keywords such as :commands, :bind, :hook, :commands, and :mode establish loading triggers automatically. These keywords are discussed below.

If a package does not have another loading trigger, :defer t prevents use-package from loading it immediately:

(use-package kirigami
  :defer t)

This prevents the package from being required during startup. Without an autoload or another loading trigger, the package will remain unloaded until something else loads it.

Idle deferral: Numeric argument (Not ideal)

A numeric value to :defer schedules the package to be loaded after Emacs has been idle for the specified number of seconds:

;; Load kirigami after Emacs has been idle for 20 seconds
(use-package kirigami
  :defer 20)

Note: This is not ideal because loading a heavy package after a few seconds will block the event loop and can cause a freeze in the user interface. If multiple packages are deferred via idle timers, they can trigger sequentially during a single idle period, increasing the delay. To prevent unprompted blocking operations, it is best practice to configure explicit autoloads, such as :hook, :bind, :commands, or :mode. (These keywords are discussed below.)

Implicit deferral: Autoload triggers (Recommended solution)

Instead of manually adding :defer t to every use-package declaration, you can use trigger keywords to configure deferred loading automatically. These keywords generate autoloads, ensuring the package evaluates only when you interact with its components:

  • :commands: Generates autoloads for specific interactive commands. Emacs loads the package when you execute the command (for example, via M-x).
  • :bind: Maps keys to commands and automatically creates autoloads for them.
  • :hook: Adds a package function to a hook and arranges for deferred loading.
  • :mode: Adds a file-pattern entry to auto-mode-alist and defers loading until the mode is needed.

Example: Autoloading on major mode file extensions

Instead of loading Markdown mode immediately, use :mode to defer it until you open a .md file:

(use-package markdown-mode
  :mode ("\\.md\\'" . markdown-mode))

Note: Specifying :defer t is unnecessary here, as :mode automatically defers package loading.

Example: Autoloading on keybindings

The :bind keyword maps keys to commands and automatically creates autoloads for them. The package remains deferred until you press one of the defined key combinations:

(use-package embark
  :bind
  (("C-." . embark-act)       
   ("C-;" . embark-dwim)       
   ("C-h B" . embark-bindings)))

Example: Autoloading specific commands

If you do not want to define global keybindings but still need to defer loading until a command is called interactively (for example, via M-x), use the :commands keyword. This generates the necessary autoloads:

(use-package embark
  :commands (embark-act
             embark-dwim
             embark-bindings))

Example: Autoloading on hook execution

Instead of eagerly loading outline-indent-minor-mode (indentation-based code folding) at startup, you can configure Emacs to load and activate it on demand. This ensures the package evaluates only when a python-mode, python-ts-mode, or yaml-ts-mode buffer is opened:

;; The outline-indent Emacs package provides a minor mode for
;; indentation-based code folding.
(use-package outline-indent
  :commands (outline-indent-minor-mode
             outline-indent-backward-same-level
             outline-indent-forward-same-level)
  :hook ((python-mode . outline-indent-minor-mode)
         (python-ts-mode . outline-indent-minor-mode)
         (yaml-ts-mode . outline-indent-minor-mode))
  :custom
  (outline-indent-ellipsis " ▼"))

This also loads outline-indent when you invoke the command manually via M-x outline-indent-minor-mode, outline-indent-backward-same-level, or outline-indent-forward-same-level.

Note: There is no need to specify :defer t separately. The :hook keyword adds outline-indent-minor-mode to python-mode-hook, python-ts-mode-hook, and yaml-ts-mode-hook.

The unconditional use-package :init block

Code inside the use-package :init section is evaluated before the package is loaded. An autoloaded function called from :init can itself cause the package to load immediately, defeating the intended deferral. Therefore, :init is generally appropriate for settings that must be established before the package loads, while package-dependent function calls belong in :config or another deferred trigger (e.g., with-eval-after-load).

(Similarly, code inside the :preface block evaluates before package loading, but it also executes during byte-compilation. This distinction makes :preface the correct location for code required to satisfy the byte-compiler, such as defvar statements, helper macro definitions, or declare-function calls that prevent compiler warnings for deferred libraries.)

Forcing immediate load with :demand

With global deferral enabled, you will occasionally encounter packages that must be loaded immediately (e.g., themes, keybinding managers, or daemon-side servers). You can override global deferral on an individual basis using :demand t:

(use-package tomorrow-night-deepblue-theme
  :demand t ; Force immediate loading
  :config
  ;; Load the tomorrow-night-deepblue theme
  (load-theme 'tomorrow-night-deepblue t))

Note: If a declaration specifies both :demand t and :defer t (or triggers), :demand t takes precedence and forces eager loading.

The use-package :after keyword

The use-package :after keyword allows you to defer the initialization of a package until one or more specified target packages have been fully loaded. This is practical for:

  • Add-on packages that extend a base minor mode.
  • Integration packages that bridge two distinct tools together.

Consider embark-consult, a package that integrates embark with consult. You only need the embark-consult package after embark and consult are loaded:

(use-package embark-consult
  :after (embark consult))

In this scenario, embark-consult remains entirely inactive during startup. Emacs won't evaluate the embark-consult configuration until it has successfully loaded both embark and consult.

Forcing immediate loading

Consider this configuration:

(use-package kirigami
  :defer t
  :custom
  (kirigami-show-menu-bar t)
  (kirigami-show-context-menu t)
  :init
  (kirigami-global-mode 1)) ; This forces immediate loading

Although :defer t is present, evaluating (kirigami-global-mode 1) during :init triggers the autoload for kirigami-global-mode. The package is therefore loaded during initialization, defeating the intended deferral.

Loading after the Emacs init phase

If you prefer to load a package after the init phase, use the :hook keyword:

(use-package kirigami
  :custom
  (kirigami-show-menu-bar t)
  (kirigami-show-context-menu t)
  :hook (after-init . kirigami-global-mode)) ; Activate after init

Using :hook (after-init . kirigami-global-mode) adds the mode to after-init-hook. This ensures the package remains deferred while Emacs evaluates the rest of your init file, guaranteeing the mode is active before the editor is ready for user interaction.

Global laziness: use-package-always-defer (Not recommended)

You can configure use-package to automatically assume :defer t for declarations that do not otherwise establish eager loading:

(setq use-package-always-defer t) ; NOT RECOMMENDED

This changes the default behavior so that use-package declarations are deferred unless a declaration explicitly requires immediate loading or establishes another loading mechanism.

Note: Global deferral breaks packages that rely on background execution, global hooks, or immediate side effects to function correctly. You will find yourself tracking down silent failures and adding :demand t throughout your initialization file to force eager loading for things like themes, modeline managers, and daemon servers. Instead of relying on a blanket setting that obscures the loading state, setting use-package-always-defer to nil and explicitly deferring packages where appropriate results in a more deterministic setup.

Diagnostics and verification

To check whether a package feature has been provided in the current Emacs session, use the featurep function. For example, for the kirigami package:

(featurep 'kirigami)
  • Evaluates to nil when the feature has not been provided.
  • Evaluates to t when the feature has been provided.

Debugging and macro expansion optimization

If you need to diagnose loading order or macro expansion issues, use-package provides built-in settings to help debugging and performance.

Setting use-package-expand-minimally to t causes the use-package macro to generate less boilerplate code. If you byte-compile your initialization file, this reduces the size of the compiled file and can yield slight performance improvements:

(setq use-package-expand-minimally t)

Setting use-package-verbose to t forces use-package to log package loading events to the *Messages* buffer. This is useful for verifying whether deferred packages are loading when expected:

(setq use-package-verbose t)

Setting use-package-enable-imenu-support to t causes imenu to see use-package declarations. This allows you to quickly jump to specific use-package blocks using M-x imenu:

(setq use-package-enable-imenu-support t)

By default, the :hook keyword automatically appends -hook if you omit it. Setting use-package-hook-name-suffix to nil disables this behavior. Disabling this forces you to write fully qualified hook names (e.g., (python-mode-hook . outline-indent-minor-mode)):

(setq use-package-hook-name-suffix nil) ; NOT RECOMMENDED

Note: It is not recommended to set use-package-hook-name-suffix to nil if you maintain a pre-existing configuration that relies on the default implicit behavior. Modifying this variable globally instantly breaks every existing :hook (mode . function) declaration that omits the -hook suffix. Unless you are building a configuration from scratch or can rewrite and validate every hook assignment in a single pass, leaving the default behavior intact is the most practical decision.

Startup profiling: measuring the impact

To benchmark startup optimizations, measure startup at a point later than the end of init-file processing.

Benchmarking startup

For an accurate startup-time measurement, read:
Measuring Emacs startup time more accurately than the built-in emacs-init-time

Profiling individual packages

use-package can collect loading statistics. Enable statistics gathering after use-package has been loaded but before the use-package declarations are evaluated:

(setq use-package-compute-statistics t)

After restarting Emacs, execute: M-x use-package-report

This displays a tabulated buffer with timing information for the use-package phases.

Advanced profiling with benchmark-init

While use-package-report offers insights into declarative package loading, the third-party package benchmark-init (available on MELPA) provides a more granular view of the entire Emacs startup process. It tracks the time spent loading individual files and executing functions across the whole initialization sequence, allowing you to see exactly where you can influence startup time.

Related links

-1:-- Optimizing Emacs Startup - Guide to Deferred Package Loading with use-package (Post James Cherti)--L0--C0--2026-09-03T16:36:20.000Z

Irreal: The Vim UserGettingBored Autocmd

Evan Hahn has an amusing post about the Vim UserGettingBored autocmd. Autocmds in Vim are essentially like hook functions in Emacs. They get fired when certain events happen in the editor.

Way back before Vim 6.0, Bram Moolenaar added the UserGettingBored autocmd to the Vim documentation. It was a joke and the command didn’t actually do anything—or rather, it was never invoked and didn’t really exist. It was like an Emacs hook that was documented but didn’t actually occur. The documentation said “When the user hits CTRL-C. Just Kidding.” In 2013 the documentation changed to “When the user presses the same key 42 times. Just kidding! :-)” and, there were various other tweaks along the way. Finally, in 2022 an unofficial plugin was created by Mike Smith that actually implemented the autocmd and did something. You can read what it was in Hahn’s post.

None of this matters at all, of course, except to provide evidence that Vim users can be as whimsical as us Emacs users. You probably won’t see anything like this in VSCode. It wouldn’t be “professional”.

-1:-- The Vim UserGettingBored Autocmd (Post Irreal)--L0--C0--2026-09-03T15:02:23.000Z

Dave Pearson: next-gh-pr.el v1.1.0

A quick little update to next-gh-pr.el. Since writing the first version I've found it to be really useful, but I've also bumped into one small issue. Sometimes I'll create a draft PR before I remember to start the changelog entry, and so then I want to insert the current PR link rather than the next one.

Now, sure, I can just go and copy the URL and create the link in the changelog, but where's the fun in that when I can solve the problem in Emacs Lisp? So, with this in mind, I've just updated the package so that, with a prefix argument to the command, it will include a link for the latest PR rather than for the next one.

This does, of course, make the name a little less correct, but I'm not going to rename the package or the command for this one minor difference in intent.

-1:-- next-gh-pr.el v1.1.0 (Post Dave Pearson)--L0--C0--2026-09-03T14:17:17.000Z

TAONAW - Emacs and Org Mode: RSI/Pinky Update

I wanted to write a short update to my Emacs pinky / RSI situation.

I am still using my Kinesis Freestyle Edge keyboard. I think I’ve decided on the Glove80 as a replacement, but the price and the inability to easily return it are giving me second thoughts. Its style is very different, and I know I will have a hard time adjusting. Still as someone pointed out to me it’s not like I’m forced to throw away my current keyboard, which is true. I see myself using my Kinesis for work on the Mac while trying the new Glove80 on Linux for blog posts and more leisure-time typing.

I’ve made a few adjustments, however:

Auto-generated description: A split ergonomic keyboard with blue backlighting is connected by a cable, positioned on a large owl-themed mousepad alongside a Logitech gaming mouse.

First, the spread between the two halves is much wider. This is something I was considering when I first got this keyboard, but I didn’t follow through with it. I got used to the wider split within a couple of days. It does make my shoulders feel better and more natural, especially when I’m standing at my desk. With that, I also started using the keyboard’s full tenting capability at all times: I used to have the keyboard flatter when sitting down.

The mouse is in the middle, and since I use it for quick bursts between otherwise typing sessions, it’s fine. When I game, I move it back to the right. I can basically ignore the entire right half of my keyboard while gaming, since most of the action happens on the left.

Gone is the Caps Lock as Ctrl. Caps is just Caps again. I’m still pressing it now and then out of habit, but for the most part, I switched back to using the original Control keys. I try to force myself to use my thumb, which means I have to curve my thumb into my left palm if I use the left side, or I just go all the way to the right and use my Ctrl key there. This is the most difficult part because it’s not just muscle memory: the Ctrl key is out of the way, and it can slow me down.

I also realized that the Windows key, which also acts as the Command key on my Mac, should be way more prominent than this keyboard allows. Thanks to PopClip on the Mac, I don’t use it much for copy-pasting on macOS (and since you have to use your mouse anyway to highlight text in Emacs, having PopClip is just a natural extension that I’m happy to have).

Speaking of the mouse, I know some of you will wrinkle your nose in disgust — but I started using it more in Emacs. Sometimes when I want to select text further down or up from where I am with the mark, it’s just easier to grab it and highlight the text this way instead of using the keyboard. I am also in the process of rediscovering the menus in Emacs (yes, those menus that all of you Emacs gurus love to hide), especially when it comes to reminding myself of useful shortcuts I keep forgetting — like registers, the mark ring, and other org-mode tricks I keep forgetting about.

My pinky still annoys me, but less. I seem to have this habit now of letting it hover over the keyboard instead of resting on the shift key. It’s almost as if I lost my ability to place my left hand correctly and naturally at times. It’s one of the things I hope the Glove80 might help with.

-1:-- RSI/Pinky Update (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-09-02T21:36:31.000Z

Irreal: The Newcomers Presets Theme

As the result of a long standing discussion on the Emacs devel list, Emacs 31 comes with what amounts to a starter kit called newcomers-presets1. The idea is to provide newcomers with a set of useful configurations to help them ease into Emacs. It is, however, much more than training wheels for n00bs. The presets described in it are ones that most experienced Emacs users are already using.

I know this because last April Sacha Chua wrote a splendid post that describes what’s in it. I somehow missed it back when she first posted it but she mentioned it in the latest Emacs News. As I’ve said before, I don’t usually write about things that she’s already covered in Emacs News but I broke that rule yesterday so I might as well break it again, especially since her post is especially worth your attention.

When newcomers-presets was first introduced I didn’t pay too much attention to it but then Prot recommended it for experienced users as well as beginners. The problem was that I was unclear on what exactly it did. Then I saw the reference to Chua’s post. It has a comprehensive list of what it does along with a explanation for each setting.

I won’t be using it because I’m not a fan of starter kits for experienced users and because even if I like everything in it today, it could change tomorrow. It’s better, in my opinion, to see what’s in it and add the things you think are useful to your own configuration. Chua includes a handy “Copy code” button with each item to make that easy.

Footnotes:

1

For some reason it’s described and implemented as a “theme” but actually has little to do with themes as we normally think of them.

-1:-- The Newcomers Presets Theme (Post Irreal)--L0--C0--2026-09-02T15:32:19.000Z

Emacs Redux: Working with Git Worktrees in Magit

I’ll admit that until fairly recently I had no idea git worktrees existed. They’ve been part of git for a decade1 and I never once needed them. Feature branches did the job just fine - create a branch, do the work, merge it, delete it, and start over.

What finally introduced me to worktrees was, of all things, AI coding agents. Tools like Claude Code create a worktree for each task, so several agents (or several sessions of the same agent) can work on the same repo in parallel without stepping on each other - or on you. Suddenly my projects directory was full of cider-this and cider-that siblings, and I figured I should understand what’s actually going on there.

Worktrees vs Branches

A branch is just a movable pointer to a commit, and making one is basically free. The catch is that a repository has a single working directory, so working on two branches means switching that directory back and forth. You know the routine: stash your half-done work (or commit it), check out the other branch, do the thing, check out the first branch again, unstash. It works, but it’s tedious, and it gets worse when the two branches leave your project in different build states and every switch means recompiling half the world.

A worktree gives you an additional working directory attached to the same repository:

$ git worktree add ../cider-smart-targeting -b smart-form-targeting

Now ~/projects/cider-smart-targeting is a full checkout of that branch, while your main checkout stays exactly where it was. The object database, refs, stashes and remotes are all shared - a worktree is not a clone, so fetching in one is fetching in all, and creating one is nearly instant. Each worktree gets its own HEAD and index, and git enforces one simple rule: a branch can only be checked out in one worktree at a time.

When are they worth it? Whenever two things need to happen at the same time: a long test run on one branch while you work on another, reviewing a PR without disturbing your half-done work, or - the reason everyone is talking about them these days - AI agents doing their thing in isolation. The price is pretty modest: your working files exist on disk more than once, and anything that isn’t tracked by git (dependencies, build caches, node_modules and friends) has to be set up again in each worktree.

If branches have never felt limiting to you, that’s fine - they didn’t for me either, for over a decade. Worktrees are one of those features you don’t miss until your workflow changes.

What About Jujutsu?

While we’re on the topic of working copies - the most interesting thing happening in version control right now is Jujutsu (jj), a git-compatible VCS that makes the problem worktrees solve mostly go away. In jj the working copy is a commit, snapshotted automatically as you work. There’s no staging area and no stash, because there’s no uncommitted state that could be lost or get in the way - switching contexts is always safe. It also has proper support for parallel working directories (jj workspace), plus an operation log that makes practically everything undoable. That last bit is part of why the AI agent crowd has taken an interest in it too.

You can use jj on top of an existing git repository (they call this a colocated repo), and your colleagues - and Magit - will keep seeing a normal git repo. I’m still just an observer here, but it’s clearly a project to keep an eye on.

Worktrees in Magit

Back to Emacs land. Magit has had worktree support for years, hiding behind Z:

Key Command Description
Z b magit-worktree-checkout Check out an existing branch in a new worktree
Z c magit-worktree-branch Create a new branch and worktree in one go
Z g magit-worktree-status Jump to another worktree’s status buffer
Z m magit-worktree-move Move a worktree
Z k magit-worktree-delete Delete a worktree

The best part is that there’s nothing else to learn. Each worktree gets its own Magit status buffer, and every Magit command operates on the worktree the current buffer belongs to. Z g is the only switching mechanism you need, and even that is just a shortcut for visiting another status buffer.

A few practical tips from my (admittedly recent) experience:

  • By default the status buffer doesn’t list your worktrees. Fix that with:
(magit-add-section-hook 'magit-status-sections-hook
                        #'magit-insert-worktrees
                        nil t)

Now every status buffer shows all worktrees of the repo, and you can hit RET on any of them to jump there.

  • Create worktrees as siblings of the main checkout with descriptive names (cider-smart-targeting next to cider), not nested inside it - nesting confuses grep, find and plenty of other tools.

  • Magit’s branch selection annotates branches that are checked out in another worktree with the worktree’s path, and refuses to check them out a second time (that’s git’s rule, not Magit being difficult). If you ever wondered why a branch “won’t check out”, that’s usually why.

  • Each worktree is its own project as far as project.el (or Projectile) is concerned, so project switching, per-project buffers and search all work naturally.

  • Anything that’s not tracked by git doesn’t come along for the ride. You’ll have to install dependencies once per worktree and start with a cold build cache. For Elisp that costs you nothing; for a big JVM or JS project it’s the main downside of the whole approach.

  • When you’re done with a worktree, delete it with Z k (or with git worktree remove from the command line). If you just delete the directory by hand, git worktree prune will clean up the leftover bookkeeping.

Closing Thoughts

These days my workflow with agents usually looks like this: an agent does its work in a worktree, I review the changes there in Magit (often while another task is running in a second worktree), and the worktree goes away once the branch is merged. Maybe one day I’ll find other uses for worktrees - we’ll see.

Are you using git worktrees - and did you discover them the same roundabout way I did? I’d love to hear about it in the comments!

That’s all I have for you today. Keep hacking (in parallel)!

  1. They were introduced in git 2.5, released all the way back in July 2015. 

-1:-- Working with Git Worktrees in Magit (Post Emacs Redux)--L0--C0--2026-09-02T14:28:00.000Z

Tim Heaney: Alpine Linux

I just installed Alpine Linux on an old laptop. Works great! Last year, I complained about all of the lightweight Linux distributions being based on Debian. Alpine is not based on Debian and is very lightweight indeed; it uses busybox in place of many GNU utilities, musl libc in place of glibc, and openrc in place of systemd. Alpine is so small that lots of folks use it for containers and things.
-1:-- Alpine Linux (Post Tim Heaney)--L0--C0--2026-09-02T00: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!