Jack Baty: Blogging with org-mode and ox-hugo again

For a few years (a few years ago) I wrote all my blog posts using one big Org mode outline and let ox-hugo generate the Markdown files for Hugo. I eventually decided it was a layer of abstraction that I didn’t need, so I fell back to writing in Markdown directly.

I’m really into using Emacs for everything again (still?), so I dusted off and updated my old ox-hugo config and here I am, typing this with Emacs in a big Org mode outline.

I have a (ya)snippet for generating the posts, like so:

# -*- mode: snippet -*-
# name: Hugo blog post
# key: blog
# uuid: blog
# --
**** TODO ${1:title}
:PROPERTIES:
:EXPORT_FILE_NAME: index.md
:EXPORT_HUGO_BUNDLE: `(format-time-string "%Y-%m-%d")`-${1:$(replace-regexp-in-string " " "-" (downcase yas-text))}
:EXPORT_HUGO_SLUG: ${1:$(replace-regexp-in-string " " "-" (downcase yas-text))}
:EXPORT_HUGO_CUSTOM_FRONT_MATTER: :coverCaption ""
:END:

#+begin_description

#+end_description

$0

The snippet prompts for a title, then creates the appropriate properties for the post.

One nice thing about this is that once the Markdown is generated, I no longer need the .org file. Writing this way is a bonus, but not a requirement.

✍️ Reply by email
-1:-- Blogging with org-mode and ox-hugo again (Post Jack Baty)--L0--C0--2026-01-21T09:55:00.000Z

Sacha Chua: Emacs and whisper.el :Trying out different speech-to-text backends and models

I was curious about parakeet because I heard that it was faster than Whisper on the HuggingFace leaderboard. When I installed it and got it running on my laptop (CPU only, no GPU), it seemed like my results were a little faster than whisper.cpp with the large model, but much slower than whisper.cpp with the base model. The base model is decent for quick dictation, so I got curious about other backends and other models.

In order to try natrys/whisper.el with other backends, I needed to work around how whisper.el validates the model names and sends requests to the servers. Here's the quick and dirty code for doing so, in case you want to try it out for yourself.

(defvar my-whisper-url-format "http://%s:%d/transcribe")
(defun whisper--transcribe-via-local-server ()
  "Transcribe audio using the local whisper server."
  (message "[-] Transcribing via local server")
  (whisper--setup-mode-line :show 'transcribing)
  (whisper--ensure-server)
  (setq whisper--transcribing-process
        (whisper--process-curl-request
         (format my-whisper-url-format whisper-server-host whisper-server-port)
         (list "Content-Type: multipart/form-data")
         (list (concat "file=@" whisper--temp-file)
               "temperature=0.0"
               "temperature_inc=0.2"
               "response_format=json"
               (concat "model=" whisper-model)
               (concat "language=" whisper-language)))))
(defun whisper--check-model-consistency () t)

Then I have this function for trying things out.

(defun my-test-whisper-api (url &optional args)
  (with-temp-buffer
    (apply #'call-process "curl" nil t nil "-s"
           url
         (append (mapcan
                  (lambda (h) (list "-H" h))
                  (list "Content-Type: multipart/form-data"))
                 (mapcan
                  (lambda (h) (list "-F" h))
                  (list (concat "file=@" whisper--temp-file)
                        "temperature=0.0"
                        "temperature_inc=0.2"
                        "response_format=verbose_json"
                        (concat "language=" whisper-language)))
                 args))
    (message "%s %s" (buffer-string) url)))

Here's the audio file. It is around 10 seconds long. I run the benchmark 3 times and report the average time.

Download

Code for running the benchmarks
(mapcar
 (lambda (group)
   (let ((whisper--temp-file "/home/sacha/recordings/whisper/2026-01-19-14-17-53.wav"))
     ;; warm up the model
     (eval (cadr group))
     (list
      (format "%.3f"
              (/ (car
                  (benchmark-call (lambda () (eval (cadr group))) times))
                 times))
      (car group))))
 '(
   ("parakeet"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 5092)))
   ("whisper.cpp base-q4_0"
    (my-test-whisper-api
     (format "http://%s:%d/inference" whisper-server-host 8642)))
   ("speaches whisper-base"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=Systran/faster-whisper-base")))
   ("speaches whisper-base.en"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=Systran/faster-whisper-base.en")))
   ("speaches whisper-small"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=Systran/faster-whisper-small")))
   ("speaches whisper-small.en"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=Systran/faster-whisper-small.en")))
   ("speaches lorneluo/whisper-small-ct2-int8"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=lorneluo/whisper-small-ct2-int8")))
   ;; needed export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1
   ("whisperx-server Systran/faster-whisper-small"
    (my-test-whisper-api
     (format "http://%s:%d/transcribe" whisper-server-host 8002)))))
3.694 parakeet
2.484 whisper.cpp base-q4_0
1.547 speaches whisper-base
1.425 speaches whisper-base.en
4.076 speaches whisper-small
3.735 speaches whisper-small.en
2.870 speaches lorneluo/whisper-small-ct2-int8
4.537 whisperx-server Systran/faster-whisper-small

I tried it with:

Looks like speaches + faster-whisper-base is the winner for now. I like how speaches lets me switch models on the fly, so maybe I can use base.en generally and switch to base when I want to try dictating in French. Here's how I've set it up to use the server I just set up.

(setq whisper-server-port 8001 whisper-model "Systran/faster-whisper-base.en"
      my-whisper-url-format "http://%s:%d/v1/audio/transcriptions")

At some point, I'll override whisper--ensure-server so that starting it up is smoother.

Benchmark notes: I have a Lenovo P52 laptop (released 2018) with an Intel Core i7-8850H (6 cores, 12 threads; 2.6 GHz base / 4.3 GHz turbo) with 64GB RAM and an SSD. I haven't figured out how to get the GPU working under Ubuntu yet.

View org source for this post

You can comment on Mastodon or e-mail me at sacha@sachachua.com.

-1:-- Emacs and whisper.el :Trying out different speech-to-text backends and models (Post Sacha Chua)--L0--C0--2026-01-20T19:21:46.000Z

punchagan: Elfeed DB backup hooks

I had a bunch of things running on my laptop – video call with screenshare, my Windows VM, Firefox with a lot of tabs, etc. And my laptop crashed! I didn’t have the time to dig into what, why and how.

Later in the day, I discovered my Elfeed’s DB was gone – blown away. :( I’m guessing the crash happened in the middle of elfeed-db-save, and the data was lost.

I’ve now added some back-up for the DB, since I intend to use Elfeed regularly.

(defvar pc/elfeed-db-save-timer nil
  "Timer for debounced elfeed database saves.")

(defun pc/elfeed-db-save-and-backup ()
  "Save the elfeed database and commit to git."
  (when (and (boundp 'elfeed-db) elfeed-db)
    (elfeed-db-save)
    (let ((default-directory elfeed-db-directory))
      (when (file-exists-p ".git")
        (call-process "git" nil "*elfeed-db-backup*" nil "add" "-A")
        (call-process "git" nil "*elfeed-db-backup*" nil "commit" "-m" "auto-backup")
        (call-process "git" nil "*elfeed-db-backup*" nil "push" "origin" "main")))))

(defun pc/elfeed-db-save-soon ()
  "Schedule a database save after 10 seconds of idle."
  (interactive)
  (when pc/elfeed-db-save-timer
    (cancel-timer pc/elfeed-db-save-timer))
  (setq pc/elfeed-db-save-timer
        (run-with-idle-timer 10 nil #'pc/elfeed-db-save-and-backup)))

;; Save and backup when tags change (elfeed-web usage)
(add-hook 'elfeed-tag-hooks   (lambda (&rest _) (pc/elfeed-db-save-soon)))
(add-hook 'elfeed-untag-hooks (lambda (&rest _) (pc/elfeed-db-save-soon)))

;; Save and backup when new entries are added
(add-hook 'elfeed-db-update-hook #'pc/elfeed-db-save-soon)
-1:-- Elfeed DB backup hooks (Post punchagan)--L0--C0--2026-01-20T17:43:00.000Z

Irreal: TMR Video

Despite what I wrote last time about tmr, I’ve come to realize that deep down I’m a timer nerd. As my family will tell you, I can be anal about following directions precisely. If the recipe says to beat the eggs for 30 seconds, I feel uncomfortable if I don’t have some way of measuring those 30 seconds more or less accurately.

Once I got my iWatch with it’s excellent and easy to use timer app, my inner timer nerd was released and now it seems I’m always using a timer for some reason or another. Given that I spend a huge amount of my time staring at a computer screen—most often in Emacs—it makes sense to be able to set and manage timers there too.

Prot to the rescue. His tmr package is just what you need to set and manage timers from within Emacs. He’s got a great video up that demonstrates tmr and its capabilities. It’s a lot more than just, “Beep after x seconds”. You can have multiple timers that you can set to fire after a given number of seconds, minutes, or hours. You can also set the timer to fire at a certain time.

You can add descriptions to each timer and display them all in a grid layout to see how much time, if any, is remaining in each timer, when it started, and when it will expire. You can also arrange to display the time remaining on each timer in the mode line if you like.

See Prot’s video for all the details. The video is 14 minutes, 32 seconds long so it should be easy to find time for it. Installation and configuration is easy so give it a try if you are also a timer nerd.

-1:-- TMR Video (Post Irreal)--L0--C0--2026-01-20T15:20:42.000Z

Christian Tietze: Emacs Carnival 2026-01: “This Year, I’ll ...”

Welcome to 2026, and a new year of Emacs Blog Carnival post!

To start the Gregorian calendar year fresh, the blogging/writing/thinking prompt of this month is:

This year, I’ll …

What will you do differently in Emacs this year? Or will you just get started? Why?

What are you excited about to explore and tinker with? What do you want to perfect?

Are there any large projects in front of you that you’ll emacs1 with confidence?

What’s a Blog Carnival, Again?

A blog carnival is a fun way to tie together a community with shared writing prompts, and marvel at all the creative interpretations of the topic of the month. I’ve provided a couple of interpretations above, but you may think of something else entirely. That’s amazing, roll with it, that’s what makes this fun!

For future Carnivals, check out the “Carnival” page on EmacsWiki . It includes instructions, and is our community space to coordinate participants and topic ideas.

Submissions

Comment below or DM/email me with your submission! I’ll collect submissions up to, and including, February 1st (Central European Time), so that every time zone has had a chance to meet the January 31st deadline.

Ordered by submission date:

  • Zimblo: This Year, I’ll de-obfuscate
  • George Jones: This Year, I’ll – comes with a handy Emacs verb definition:

    Emacso, -are, -avi, -atus (verb, 1st conjugation)

    emacso, emacsas, emacsat, emacsamus, emacsatis, emacsant

    • Cotidie, cum discipulis meis, emacso ad software melius discendum. (Every day, with my students, I emacs to learn better software.)
  1. I’m trying to establish a verb here. 


Hire me for freelance macOS/iOS work and consulting.

Buy my apps.

Receive new posts via email.

-1:-- Emacs Carnival 2026-01: “This Year, I’ll ...” (Post Christian Tietze)--L0--C0--2026-01-20T08:17:09.000Z

Evan Moses: Moving My Pi to an SSD

TL;DR

After setting a project aside for 5 years, I got my RPi4 booting from an SSD and running a 64bit OS by tweakingthe USB_MSD_STARTUP_DELAY bootloader config parameter.

I run Home Assistant on a Raspberry Pi 4, using Docker Compose on Raspbian. This has worked great for me for years, andoverall been very stable, but there’s been a Sword of Damocles hanging over the project: it was all running off the SDcard. Eventually the SD Card would accumulate too many writes and fail. I had all the logging set upto use log2ram to minimize wear-and-tear, but this was a band-aid.

In 2020 the RPi got a firmware update that allowed booting from USB insteadof the SD card, which allows using an SSD with an external enclosure. In 2021 I gave a shot to moving to SSD; therewere a number of guides available on how to do this, so I bought:

And I gave a shot to copying the current SD card onto the SSD and booting up the Pi.

It didn’t work

I tried this a number of times and different ways. The SD card copied fine and if I hot-plugged the drive into therunning Pi, I was able to mount it and browse the files. I triple checked the boot order, but if I leftthe SD card in the Pi it would boot off the SD card, and if I removed the SD card and let ittry to boot…nothing.

At the time, I decided everything was working good ’nuf and I’d just restore from backup if the SD card crapped out. I put thedrive and enclosure up on the Shelf of Despair with other half-finished projects.

Whoops, I need 64 bits

So, as with many homelab projects that Just Work…I let it keep on just working and left it alone. In 2025, though,Home Assistantannouncedthat it was dropping support for 32-bit OSes, which meant if I wanted to continue getting updates for Home Assistant I’dneed to reinstall my OS. This seemed like a good time to go ahead and move to an SSD.

I started by using RPi Imager to install Raspbian 64-bit to the same SSD with the same enclosure I was using before. Itried to boot off this perfectly clean install…and it still didn’t work. Same problems as before. After diggingaround online some more, I found that there’s a relatively common problem: the SSD enclosure doesn’t power up andcome online as quickly as the bootloader is expecting, and the RPi gives up looking for a USB drive. There’s a settingthat can fix this

USB_MSD_STARTUP_DELAY

If defined, delays USB enumeration for the given timeout after the USB host controller has initialised. If a USB harddisk drive takes a long time to initialise and triggers USB timeouts then this delay can be used to give the driveradditional time to initialise. It may also be necessary to increase the overall USB timeout(USB_MSD_DISCOVER_TIMEOUT).

To see the current settings, you can run rpi-eeprom-config. I ran sudo rpi-eeprom-config --edit to update thesetting according to whatever forum post I finally landed on (followed by a reboot to install the updated firmwareconfig) and it now looks like this:

[all]BOOT_UART=0WAKE_ON_GPIO=1POWER_OFF_ON_HALT=0[all]BOOT_ORDER=0xf41USB_MSD_STARTUP_DELAY=20000USB_MSD_BOOT_MAX_RETRIES=5

The BOOT_ORDER is specified here,and the default 0xf41 means “Try SD first, followed by USB-MSD then repeat (default if BOOT_ORDER is empty)”. TheUSB_MSD_STARTUP_DELAY is the value I upped to 20s, and it, along with the max_retries, seems to have solved the problem forme. I didn’t experiment further to see if a lower value would work, but it’s definitely somewhere between 5s and 20sfor my combination of hardware.

I copied over the docker directory (which contained the MariaDB files and all my configs) from my backup to the newdrive, as well as a few other services, and now everything’s running happily again.

-1:-- Moving My Pi to an SSD (Post Evan Moses)--L0--C0--2026-01-20T00:00:00.000Z

Sacha Chua: 2026-01-19 Emacs news

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 comment on Mastodon or e-mail me at sacha@sachachua.com.

-1:-- 2026-01-19 Emacs news (Post Sacha Chua)--L0--C0--2026-01-19T19:46:50.000Z

Marcin Borkowski: Toy train timetable clock

My son loves trains. Once in a while, we’re playing with electric toy trains. Sometimes this means just having them move around and use switches to change their routes, and sometimes it means building two or more stations and moving actual (toy) people and (toy) wares between stations. Recently, we came up with an idea to level up our game.
-1:-- Toy train timetable clock (Post Marcin Borkowski)--L0--C0--2026-01-19T18:46:55.000Z

TAONAW - Emacs and Org Mode: org-mode capture: menu inside a menu

Over the weekend, I modified my org-capture templates again. I was curious to see if I could create a second-level menu in org-mode capture under my already existing menu items - and as it turns out, it’s possible, and also pretty straightforward. Here’s the start of the setting (keep in mind it’s not complete, don’t just copy-paste this):

    (setq org-capture-templates
          (quote (
                  ("w" "work") 
                  ("wt" "work-ticket") 
                  ("wta" "work-ticket department A" entry
                   (file "~/Sync/Work/department A.org") (file "~/Sync/Templates/temp-ticket.org"):prepend t)

This will result in the following:

    Select a capture template
    ===========================
    
    [w]... work...

then:

    Select a capture template
    ===========================
    
    [wt]... work ticket...

then:

    Select a capture template
    ===========================
    
    [wta] work-ticket department A

I should explain this further, probably first explaining why I need such a complicated system of menus within menus, but every time I sit down to explain I run out of time. I wanted to put it out there first, and I’ll come around to explain it soon, hopefully.

-1:-- org-mode capture: menu inside a menu (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-01-19T17:49:04.000Z

Irreal: The Success of Markdown

Anil Dash has a long post on How Markdown Took Over The World. We Org mode users may have a few problems with that depiction but it is fair to say that Markdown usage has become ubiquitous. We see it everywhere, even in the most unexpected places.

The most interesting part of Dash’s post, to me, is his history of Markdown. In the tradition of open source software, it began as an itch on the part of a user. John Gruber wanted an easier way of writing his blog Daring Fireball. The original markdown was a perl script that translated some simple markup text into HTML.

In retrospect, I’d say that the real genius of Markdown is that it’s plain text. That means you don’t need a bespoke application to use it. You can use your preferred method of entering text to write your Markdown source. It’s other winning aspect is that nobody “owns” it. Anybody can use it or incorporate into their application without fuss or fee.

We Org mode aficionados prefer Org markup, of course. I’m inclined to think that the syntax of Org mode versus Markdown isn’t that much of an issue but others strongly disagree. For me, the advantage of Org mode is it’s power—to which some dialects of Markdown are slowly catching up—and it’s close integration with Emacs, which is where I do all my writing.

The other big advantage of Org mode is that there is only one. Sadly, Markdown has several incompatible dialects so it’s hard to know which version to master or use. On the other hand, Markdown is not tied to any particular application so you can use it anywhere. It is technically true that you can write Org mode in any editor and translate it to almost any target with Pandoc but as a practical matter if you want to use Org mode, you need to use Emacs.

This post is an ecumenical moment where we celebrate the huge advantages of Markdown and Org mode over their mostly proprietary competitors. We may argue over which is best but we agree that they’re better than their alternatives.

-1:-- The Success of Markdown (Post Irreal)--L0--C0--2026-01-19T15:33:49.000Z

Chris Maiorana: Workout tracking with Org Mode bounced up to HTML file

As we are now full swing into New Years resolution season, I’m sure you are all thinking about getting your fitness game in order. If not, then why not?

For the past seven years I’ve carried a little notebook for tracking workouts and exercises at home or at the gym. What I love about the notebook is: I don’t have to think about what to do at the gym because it’s already written down, I can see what I did last time and make adjustments as needed, and I don’t have to carry my phone.

But recently, I wanted to see if I could take recent notebook data, put it into Org Mode, and see if I could better spot trends using historic data.

The notebook is easy for capturing data in the moment, but for processing later I needed something that does a little math.

With Org Mode, I can transfer the data from the notebook to a digital medium, and then do some nice post-processing to visualize the results.

I did a video demonstrating this, and if you’d like to see the code, I put it on GitHub here. This code is just an example of what can be done. Something like this would likely require some adjustment to connect with your personal workout routine.

If you have any comments or questions, feel free to leave a comment below.

As always, be sure to check out my other projects:

The post Workout tracking with Org Mode bounced up to HTML file appeared first on Chris Maiorana.

-1:-- Workout tracking with Org Mode bounced up to HTML file (Post Chris Maiorana)--L0--C0--2026-01-19T12:46:20.000Z

Magnus: Trying eglot, again

I've been using lsp-mode since I switched to Emacs several years ago. When eglot made into Emacs core I used it very briefly but quickly switched back. Mainly I found eglot a bit too bare-bones; I liked some of the bells and whistles of lsp-ui. Fast-forward a few years and I've grown a bit tired of those bells and whistles. Specifically that it's difficult to make lsp-ui-sideline and lsp-ui-doc work well together. lsp-ui-sidedline is shown on the right side, which is good, but combining it with lsp-ui-doc leads to situations where the popup covers the sideline. What I've done so far is centre the line to bring the sideline text out. I was playing a little bit with making the setting of lsp-ui-doc-position change depending on the location of the current position. It didn't work that well though so I decided to try to find a simpler setup. Instead of simplifying the setup of lsp-config I thought I'd give eglot another shot.

Basic setup

I removed the statements pulling in lsp-mode, lsp-ui, and all language-specific packages like lsp-haskell. Then I added this to configure eglot

(use-package eglot
  :ensure nil
  :custom
  (eglot-autoshutdown t)
  (eglot-confirm-server-edits '((eglot-rename . nil)
                                (t . diff))))

The rest was mainly just switching lsp-mode functions for eglot functions.

lsp-mode function eglot function
lsp-deferred eglot-ensure
lsp-describe-thing-at-point eldoc
lsp-execute-code-action eglot-code-actions
lsp-find-type-definition eglot-find-typeDefinition
lsp-format-buffer eglot-format-buffer
lsp-format-region eglot-format
lsp-organize-imports eglot-code-action-organize-imports
lsp-rename eglot-rename
lsp-workspace-restart eglot-reconnect
lsp-workspace-shutdown eglot-shutdown

I haven't verified that the list is fully correct yet, but it looks good so far.

The one thing I might miss is lenses, and using lsp-avy-lens. However, everything that I use lenses for can be done using actions, and to be honest I don't think I'll miss the huge lens texts from missing type annotations in Haskell.

Configuration

One good thing about lsp-mode's use of language-specific packages is that configuration of the various servers is performed through functions. This makes it easy to discover what options are available, though it also means not all options may be available. In eglot configuration is less organised, I have to know about the options for each language server and put the options into eglot-workspace-configuration myself. It's not always easy to track down what options are available, and I've found no easy way to verify the settings. For instance, with lsp-mode I configures HLS like this

(lsp-haskell-formatting-provider "fourmolu")
(lsp-haskell-plugin-stan-global-on nil)

which translates to this for eglot

(setq-default eglot-workspace-configuration
              (plist-put eglot-workspace-configuration
                         :haskell
                         '(:formattingProvider "fourmolu"
                           :plugin (:stan (:global-on :json-false)))))

and I can verify that this configuration has taken effect because I know enough about the Haskell tools.

I do some development in Python and I used to configure pylsp like this

(lsp-pylsp-plugins-mypy-enabled t)
(lsp-pylsp-plugins-ruff-enabled t)

which I think translates to this for eglot

(setq-default eglot-workspace-configuration
              (plist-put eglot-workspace-configuration
                         :pylsp
                         '(:plugins (:ruff (:enabled t)
                                     :mypy (:enabled t)))))

but I don't know any convenient way of verifying these settings. I'm simply not familiar enough with the Python tools. I can check the value of eglot-workspace-configuration by inspecting it or calling eglot-show-workspace-configuration but is there really no way of asking the language server for its active configuration?

Closing remark

The last time I gave up on eglot very quickly, probably too quickly to be honest. I made these changes to my configuration over the weekend, so the real test of eglot starts when I'm back in the office. I have a feeling I'll stick to it longer this time.

-1:-- Trying eglot, again (Post Magnus)--L0--C0--2026-01-19T07:00:00.000Z

Protesilaos Stavrou: Emacs: easily set timers with TMR

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

In this ~15-minute video I demonstrate a package of mine called tmr (pronounced as an acronym or as “timer”). It uses a simple notation to set the duration of a timer at the minibuffer prompt. Once the timer elapses, Emacs shows a notification. The desktop environment will also include one, as well as an audio alert (those are configurable). Timers can optionally have a description. They may also be listed in a tabulated/grid view, which makes it easier to work with them (to edit their description, reschedule them, etc.). Running timers may also be displayed on the mode line.

Sample configuration

(use-package tmr
  :ensure t
  :config
  (define-key global-map (kbd "C-c t") #'tmr-prefix-map)
  (setq tmr-sound-file "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
        tmr-notification-urgency 'normal
        tmr-description-list 'tmr-description-history))

TMR sources

-1:-- Emacs: easily set timers with TMR (Post Protesilaos Stavrou)--L0--C0--2026-01-19T00:00:00.000Z

Donovan R.: 💭 What Happens When the Apprentice Magician Plays with God’s Wands?

Building on my previous posts about meta-understanding and on my relationship with AI tools, today I’m sharing a bit of a conflicting interest.

In recent months, every noise you hear seems to carry AI with it. It is everywhere, and it won’t stop or go away anytime soon. We can’t really deny what’s in front of us. The capabilities of AI tools in the last few months are completely insane. I’m not talking only about content creation tools, but also deeply technical ones.

As much as I’m concerned with privacy and all that, it’s impossible to ignore that AI is here and changing everything right now—for better or for worse. I often remind myself of the movie Don’t Look Up when I’m too stubborn to see what’s going on.

The Grail for Developers

One particular tool that piqued my interest is Google’s code wiki (an AI-powered codebase explorer), where you can dive into repositories and learn whatever you want using an AI assistant. As a software developer, this feels like a grail. You cherry-pick what you want, and it’s delivered exactly as you need it.

I cannot imagine the amount of knowledge you can pour from that. Agentic AI systems (autonomous agents) can do even more, but hey, this is already here and free to use. What I want to say is that possibilities are opening everywhere for almost anyone with the right interest. Intent is no longer constrained by time or resources the way it used to be. With tools that can narrate a codebase or explain a complex paradigm in seconds, the floor of the ocean feels closer than ever. We are no longer limited by the speed of our reading, but by the clarity of our desire.

This power excites me as a developer, yet it’s exactly why my dilemma starts to grow.

Lisp, AI, and Meta-Understanding

Lisp and AI share a kind of DNA: they are both declarative. They allow us to bypass the “how” and focus on the “intent.” But Lisp is a declarative sun—it illuminates the logic. AI is a declarative shadow—it gives you the result, but swallows the entire process.

While AI helps me grasp the latest details of almost anything, the AI itself remains a black box—a closed room where the lights are off. We know little about how it truly works, and we can’t accurately predict what it will do. We are using an opaque mind to create a transparent world: an apprentice magician playing with God’s tools.

If I use a tool I do not understand to explain a tool I want to master, where does the meta-understanding actually live?

Closing Thought

“The one who invented the boat also invented the shipwreck,” as the saying goes, but the unease persists for me. There is a new light, a very strong one. It is the future, but I can’t shake the uncomfortable feeling I have about this “new light.”

I’m interested to know what people think about this. Do you share similar feelings? What’s your take on all this?

-1:-- 💭 What Happens When the Apprentice Magician Plays with God’s Wands? (Post Donovan R.)--L0--C0--2026-01-18T18:47:49.000Z

Irreal: Regulating Your Writing Workflow With Emacs

A few days ago, Chris Maiorana wrote an article, The 10-Commit Rule: how git version control can improve writing quality, about how he uses Git and Emacs to regulate his writing workflow. The idea is to track how many changes he’s made to his text since the last commit and to stop and review his work when he reaches about 250 changes, deletions, or additions. The advantage of this system, he says, is that it keeps you from burning out in an initial burst of enthusiasm by forcing you to stop periodically and see where you are.

To help him with this, he has a mode line display that shows him how many changes he’s made since the last commit and how many commits he’s made today. It seems like a system that could be useful to many writers. The problem is, he didn’t explain how he got the numbers for his mode line display.

Now, however, he has a new post that explains how he generates that display. The TL;DR is that he interrogates Git to get both numbers. The number of commits is pretty easy, of course, but getting the number of changes is a bit trickier. Basically, he runs a script that does a git-diff and pipes the results through grep and wc to extract and count the “words” that have changed. To keep from running the script continuously, he caches the results and updates them only every 30 seconds.

I’m not sure I’d like using his exact system but I must admit that I do pretty much the same thing without the software assist. Every so often, I stop, reread and edit what I’ve written, and, if I remember, commit what I have so far. As any writer will tell you, there are as many writing methods as there are writers so whether Maiorana’s method works for you will be a matter of your preferences.

-1:-- Regulating Your Writing Workflow With Emacs (Post Irreal)--L0--C0--2026-01-18T15:43:09.000Z

James Dyer: Speed Reading in Emacs: Building an RSVP Reader

I recently came across a fascinating video titled “How Fast Can You Read? - Speed Reading Challenge” that demonstrated the power of RSVP (Rapid Serial Visual Presentation) for speed reading. The concept is quite nice and simple and I vaguely remember seeing something about it a few years back. Instead of your eyes scanning across lines of text, words are presented one at a time in a fixed position. This eliminates the mechanical overhead of eye movements and can dramatically increase reading speed!

So, I immediately wondered, could I build this into Emacs?, actually no, firstly I thought, are there any packages for Emacs that can do this?, of course there are!, the spray package from MELPA is a more mature, feature-rich option if you’re looking for production-ready RSVP reading in Emacs, and also there is speedread. However, there’s something satisfying about having a compact, single-function solution that does exactly what you need, so lets see if I can build one!

RSVP works by displaying words sequentially in the same location on screen. Your eyes remain stationary, focused on a single point, while words flash by at a controlled pace. This technique can boost reading speeds to 300-600+ words per minute, compared to typical reading speeds of 200-300 WPM.

The key innovation is the Optimal Recognition Point (ORP) - typically positioned about one-third into each word. This is where your eye naturally fixates when reading. By aligning each word’s ORP at the same screen position, RSVP creates an optimal visual flow.

Given Emacs’ extensive text processing capabilities, this sounds something that Emacs could eat for breakfast. Here is what I came up with:

Here is a quick video of my implementation:

and the defun:

(defun rsvp-minibuffer ()
 "Display words from point (or mark to point) in minibuffer using RSVP.
Use f/s for speed, [/] for size, b/n to skip, SPC to pause, q to quit."
 (interactive)
 (let* ((start (if (region-active-p) (region-beginning) (point)))
 (end (if (region-active-p) (region-end) (point-max)))
 (text (buffer-substring-no-properties start end))
 (wpm 350) (font-size 200) (orp-column 20)
 (word-positions '()) (pos 0) (i 0)
 (message-log-max nil)) ; Disable message logging
 ;; Build word positions list
 (dolist (word (split-string text))
 (unless (string-blank-p word)
 (when-let ((word-start (string-match (regexp-quote word) text pos)))
 (push (cons word (+ start word-start)) word-positions)
 (setq pos (+ word-start (length word))))))
 (setq word-positions (nreverse word-positions))
 ;; Display loop
 (while (< i (length word-positions))
 (let* ((word (car (nth i word-positions)))
 (word-pos (cdr (nth i word-positions)))
 (word-len (length word))
 (delay (* (/ 60.0 wpm)
 (cond ((< word-len 3) 0.8) ((> word-len 8) 1.3) (t 1.0))
 (if (string-match-p "[.!?]$" word) 1.5 1.0)))
 (orp-pos (/ word-len 3))
 (face-mono `(:height ,font-size :family "monospace"))
 (face-orp `(:foreground "red" :weight normal ,@face-mono))
 (padded-word (concat
 (propertize (make-string (max 0 (- orp-column orp-pos)) ?\s) 'face face-mono)
 (propertize (substring word 0 orp-pos) 'face face-mono)
 (propertize (substring word orp-pos (1+ orp-pos)) 'face face-orp)
 (propertize (substring word (1+ orp-pos)) 'face face-mono))))
 (goto-char (+ word-pos word-len))
 (message "%s" padded-word)
 (pcase (read-event nil nil delay)
 (?f (setq wpm (min 1000 (+ wpm 50))))
 (?s (setq wpm (max 50 (- wpm 50))))
 (?\[ (setq font-size (max 100 (- font-size 20))))
 (?\] (setq font-size (min 400 (+ font-size 20))))
 (?b (setq i (max 0 (- i 10))))
 (?n (setq i (min (1- (length word-positions)) (+ i 10))))
 (?\s (read-event (format "%s [PAUSED - WPM: %d]" padded-word wpm)))
 (?q (setq i (length word-positions)))
 (_ (setq i (1+ i))))))))

The function calculates the ORP as one-third through each word and highlights it in red. By padding each word with spaces, the ORP character stays perfectly aligned in the same column, creating that crucial stationary focal point.

To ensure pixel-perfect alignment, the function explicitly sets a monospace font family for all displayed text. Without this, proportional fonts would cause the ORP to drift slightly between words, although I think at times there is a little waddle, but it is good enough.

Also, Not all words are created equal:

  • Short words (< 3 characters) display 20% faster
  • Long words (> 8 characters) display 30% slower
  • Words ending in punctuation (.!?) get 50% more time

This mimics natural reading rhythms where you’d naturally pause at sentence boundaries.

While reading, you can try these kebindings: (which I borrowed off spray)

  • f / s - Speed up or slow down (±50 WPM)
  • [ / ] - Decrease or increase font size
  • b / n - Skip backward or forward by 10 words
  • SPC - Pause (press any key to resume)
  • q - Quit
  • C-g - Emergency quit

Also The function tracks each word’s position in the original buffer and updates point as you read. This means:

  • You can see where you are in the text
  • When you quit, your cursor is at the last word you read
  • You can resume reading by running the function again

To use it, simply:

  1. Position your cursor where you want to start reading (or select a region)
  2. Run M-x rsvp-minibuffer
  3. Watch the words flow in the minibuffer

The function works from point to end of buffer, or if you have an active region, it only processes the selected text.

If you’re curious about RSVP reading, drop this function into your Emacs config and give it a try. Start at 300-350 WPM and see how it feels. You might be surprised at how much faster you can consume text when your eyes aren’t constantly moving across the page.

The code is simple enough to customize - adjust the default WPM, change the ORP colour, modify the timing multipliers, or add new controls. That’s the beauty of Emacs, if you can imagine it, you can build it.

-1:-- Speed Reading in Emacs: Building an RSVP Reader (Post James Dyer)--L0--C0--2026-01-18T10:30:00.000Z

Protesilaos Stavrou: Emacs: notmuch-indicator version 1.3.0

This package renders an indicator with an email count of the notmuch index on the Emacs mode line. The underlying mechanism is that of notmuch-count(1), which is used to find the number of items that match the given search terms. In practice, the user can define one or more searches and display their counters. These form a string which realistically is like: @50 😱1000 ♥️0 for unread messages, bills, and fan letters, respectively.

Below are the release notes.


1.3.0 on 2026-01-18

This version adds quality-of-life refinements to a stable package.

The notmuch-indicator-mode sets up the notmuch-after-tag-hook

The indicator will be updated whenever a message’s tags change. This way users do not need to rely on the timer-based method that we have always had.

The notmuch-indicator-refresh-count can be set to nil

Doing so has the effect of disabling the timer-based refresh of the indicator. It will now be updated only when some event happens, such as with the aforementioned change to tags or after the invocation of any of the commands listed in the user option notmuch-indicator-force-refresh-commands.

More configuration file paths

When checking for the notmuch configuration file, we now also consider these two filesystem paths:

  • $HOME/.config/notmuch/$NOTMUCH_PROFILE/config
  • $HOME/.config/notmuch/default/config

Thanks to Yejun Su for the contribution in pull request 6: https://github.com/protesilaos/notmuch-indicator/pull/6.

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

-1:-- Emacs: notmuch-indicator version 1.3.0 (Post Protesilaos Stavrou)--L0--C0--2026-01-18T00:00:00.000Z

Sacha Chua: Emacs: Updating a Mailchimp campaign using a template, sending test e-mails, and scheduling it

I'm helping other volunteers get on board with doing the Bike Brigade newsletter. Since not everyone has access to (or the patience for) MailChimp, we've been using Google Docs to draft the newsletter and share it with other people behind the scenes. I've previously written about getting a Google Docs draft ready for Mailchimp via Emacs and Org Mode, which built on my code for transforming HTML clipboard contents to smooth out Mailchimp annoyances: dates, images, comments, colours. Now I've figured out how to update, test, and schedule the MailChimp campaign directly from Emacs so that I don't even have to go into the MailChimp web interface at all. I added those functions to sachac/mailchimp-el.

I used to manually download a ZIP of the Google Docs newsletter draft. I didn't feel like figuring out authentication and Google APIs from Emacs, so I did that in a NodeJS script instead. convert-newsletter.js can either create or download the latest newsletter doc from our Google Shared Drive. (google-api might be helpful if I want to do this in Emacs, not sure.) If I call convert-newsletter.js with the download argument, it unpacks the zip into ~/proj/bike-brigade/temp_newsletter, where my Emacs Lisp function for processing the latest newsletter draft with images can turn it into the HTML to insert into the HTML template I've previously created. I've been thinking about whether I want to move my HTML transformation code to NodeJS as well so that I could run the whole thing from the command-line and possibly have other people run this in the future, or if I should just leave it in Emacs for my convenience.

Updating the campaign through the Mailchimp API means that I don't have to log in, replicate the campaign, click on the code block, and paste in the code. Very nice, no clicks needed. I also use TRAMP to write the HTML to a file on my server (my-bike-brigade-output-file is of the form /ssh:hostname:/path/to/file) so that other volunteers can get a web preview without waiting for the test email.

(defun my-brigade-next-campaign (&optional date)
  (setq date (or date (org-read-date nil nil "+Sun")))
  (seq-find
   (lambda (o)
     (string-match (concat "^" date)
                   (alist-get 'title (alist-get 'settings o))))
   (alist-get 'campaigns (mailchimp-campaigns 5))))

(defvar my-bike-brigade-output-file nil)

(defun my-brigade-download-newsletter-from-google-docs ()
  "Download the newsletter from Google Docs and puts it in ~/proj/bike-brigade/temp_newsletter/."
  (interactive)
  (let ((default-directory "~/proj/bike-brigade"))
    (with-current-buffer (get-buffer-create "*Newsletter*")
      (erase-buffer)
      (display-buffer (current-buffer))
      (call-process "node" nil t t "convert-newsletter.js" "download"))))

(defun my-brigade-create-or-update-campaign ()
  (interactive)
  (let* ((date (org-read-date nil nil "+Sun"))
         (template-name "Bike Brigade weekly update")
         (list-name "Bike Brigade")
         (template-id
          (alist-get
           'id
           (seq-find
            (lambda (o)
              (string= template-name (alist-get 'name o)))
            (alist-get 'templates (mailchimp--request-json "templates")))))
         (list-id (seq-find
                   (lambda (o)
                     (string= list-name
                              (alist-get 'name o)))
                   (alist-get 'lists (mailchimp--request-json "lists"))))
         (campaign (my-brigade-next-campaign date))
         (body `((type . "regular")
                 (recipients (list_id . ,(alist-get 'id list-id)))
                 (settings
                  (title . ,date)
                  (subject_line . "Bike Brigade: Weekly update")
                  (from_name . "Bike Brigade")
                  (reply_to . "info@bikebrigade.ca")
                  (tracking
                   (opens . t)
                   (html_clicks . t))))))
    (unless campaign
      (setq campaign (mailchimp--request-json
                      "/campaigns"
                      :method "POST"
                      :body
                      body)))
    ;; Download the HTML
    (my-brigade-download-newsletter-from-google-docs)
    ;; Upload to Mailchimp
    (mailchimp-campaign-update-from-template
     (alist-get 'id campaign)
     template-id
     (list
      (cons "main_content_area"
            (my-brigade-process-latest-newsletter-draft-with-images
             date))))
    (when my-bike-brigade-output-file
      (with-temp-file my-bike-brigade-output-file
        (insert (alist-get 'html (mailchimp--request-json (format "/campaigns/%s/content" (alist-get 'id campaign)))))))
    (message "%s" "Done!")))

Now to send the test e-mails…

(defvar my-brigade-test-emails nil "Set to a list of e-mail addresses.")
(defun my-brigade-send-test-to-me ()
  (interactive)
  (mailchimp-campaign-send-test-email (my-brigade-next-campaign) user-mail-address))

(defun my-brigade-send-test ()
  (interactive)
  (if my-brigade-test-emails
      (mailchimp-campaign-send-test-email (my-brigade-next-campaign) my-brigade-test-emails)
    (error "Set `my-brigade-test-emails'.")))

And schedule it:

(defun my-brigade-schedule ()
  (interactive)
  (let ((sched (format-time-string "%FT%T%z" (org-read-date t t "+Sun 11:00") t))
        (campaign (my-brigade-next-campaign)))
    (mailchimp-campaign-schedule campaign sched)
    (message "Scheduled %s" (alist-get 'title (alist-get 'settings campaign)))))

Progress, bit by bit! Here's a screenshot showing the Google Docs draft on one side and my web preview in the other:

2026-01-17_13-00-27.png
Figure 1: Google Docs and Mailchimp campaign preview

It'll be even cooler if I can get some of this working via systemd persistent tasks so that they happen automatically, or have some kind of way for the other newsletter volunteers to trigger a rebuild. Anyway, here's https://github.com/sachac/mailchimp-el in case the code is useful for anyone else.

This is part of my Emacs configuration.
View org source for this post

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

-1:-- Emacs: Updating a Mailchimp campaign using a template, sending test e-mails, and scheduling it (Post Sacha Chua)--L0--C0--2026-01-17T17:59:01.000Z

Irreal: Bending Emacs 10: Agent Shell

Álvaro Ramírez has a new Bending Emacs video up. This time it’s about his agent-shell app that serves as uniform Emacs interface to LLM agents supporting the Agent Client Protocol (ACP).

I’m not interested in LLM technology so you almost never see an Irreal post about it. I don’t know—and therefore have no opinion on—if it’s something real or just another venture capitalist fever dream. Still, if you are interested in LLMs, it’s nice to have an app like agent-shell that provides a uniform interface to most (all?) of them.

There’s a huge number of options and details to negotiate so an app like agent-shell is a real boon. Almost none of those options involve picking which shell you want to use. You simply choose one from a list of supported shells and work within that shell for the rest of your session.

There are way too many details for me to cover here. The video itself is 36 minutes, 34 seconds so there’s a lot of content to cover. If you are interested in LLMs, and especially if you use more than one, you should definitely watch this video and download agent-shell. It’s available on Melpa so installation is easy. Like all of Ramírez’s work, agent-shell seems like a well engineered app and it’s free so you have nothing to lose by trying it.

-1:-- Bending Emacs 10: Agent Shell (Post Irreal)--L0--C0--2026-01-17T15:40:52.000Z

Protesilaos Stavrou: Emacs: doric-themes version 0.6.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 0.6.0 on 2026-01-17

This version adds support for more packages, while it revises some of the faces that were already covered.

Enhanced completion interface

The minibuffer prompt used by the command doric-themes-select now groups themes by their light or dark type. The current theme is at the top.

Avy highlights are easier to spot

The highlights generated by the various commands of the avy package now have a slightly more intense background+foreground colour combination. It should be easier to spot and to differentiate from other highlights such as that of hl-line-mode and the mouse hover effect over links.

Support for all the tmr faces

My tmr package styles timers in its grid/tabulated interface as well as on the mode line. All these now get colours that come directly from the active Doric theme. Before, the colours were defined only in the tmr source code: they were “okay” (because they are based on my modus-themes) but not stylistically optimal.

Support for ruler-mode

The built-in ruler-mode draws a ruler at the top of the current buffer. All of its faces now use appropriate colours.

Trailing spaces have a more refined colour

All packages that have a face that is about highlighting trailing spaces now get a red colour value that is more appropriate for each Doric theme.

SHR buffers can use proportionately spaced fonts

I removed an override for the built-in shr-text face, which was making the nov package display its buffers in a monospaced font.

Thanks to Marcus Kammer for telling that nov-mode buffers were not proportionately spaced by default. This was done in issue 23: https://github.com/protesilaos/doric-themes/issues/23.

-1:-- Emacs: doric-themes version 0.6.0 (Post Protesilaos Stavrou)--L0--C0--2026-01-17T00:00:00.000Z

Irreal: Multiple Editing Instances Of A Single File

Emacs has a couple of builtin ways of dealing with separate windows that are looking at the same file. The default method is to simply “open” the file again in a separate window. In this case you have two views into the same file. The windows may be looking at different parts of the file but any change made in one window appears in the other as well because both windows are using the same buffer.

Alternatively, you can open the second window by cloning the first buffer. That creates a second buffer with the same text and properties but they are distinct buffers that can have different point values, different narrowing, different major modes, different markers, different overlays, and different local variables. Nevertheless, any text or text property change made in one buffer is reflected in the other. Take a look at the Emacs manual for the details.

You’d think that those two options would cover whatever you needed to do but Sacha Chua has a post that describes a case not covered by either. In this case the user wants to display an SVG file in one window and the XML that generates it in another. The problem here is that the image is displayed with text properties so toggling the display in one window will cause it to affect the other window as well even if the other window is a cloned buffer.

Chua shows how to solve this by writing a bit of Elisp. The TL;DR is that she bypassed the code that checks if the target file is already opened in another buffer and simply creates a new buffer unconditionally. Of course, now you have to worry about syncing the text between the two buffers. Chua solves this by turning on global-auto-revert-mode so that when one file is saved, the other gets updated.

I’m always prattling about how having the Emacs source available from within the executable image coupled with the ability to make on the fly changes is one of Emacs’ magic powers. Chua’s solution is a nice example of this.

-1:-- Multiple Editing Instances Of A Single File (Post Irreal)--L0--C0--2026-01-16T16:15:57.000Z

punchagan: Safari and invalid HTTP/2 headers

Elfeed-offline currently has a Dream web server which acts as a proxy server in front of Elfeed’s Emacs simple-httpd server.

simple-httpd supports HTTP/1.1 protocol, while Dream provides transparent upgrading of connections to HTTP/2 — if the client can handle HTTP/2 and the connection is using HTTPS, it is transparently upgraded to HTTP/2.

My proxying code was too simplistic in forwarding the headers too along with the content received from the simple-httpd server. Some of the HTTP/1 headers are no longer valid in HTTP/2. And, Safari (and curl) strictly adhere to the protocol and fail if there are invalid headers. Curl, for instance, fails with the following error:

< HTTP/2 200
< server: simple-httpd (Emacs 30.1)
< date: Fri, 16 Jan 2026 10:57:25 GMT
* Invalid HTTP header field was received: frame type: 1, stream: 1, name: [connection], value: [keep-alive]
* [HTTP2] [1] received invalid frame: FRAME[HEADERS, len=77, hend=1, eos=0], error -531: Invalid HTTP header field was received
* HTTP/2 stream 1 was not closed cleanly: unknown (err 4294966765)
* Connection #0 to host 192.168.1.5 left intact
curl: (92) Invalid HTTP header field was received: frame type: 1, stream: 1, name: [connection], value: [keep-alive]

This was causing issues for @Feyorsh who was trying out elfeed-offline with Safari. Thanks for taking the time to debug the problem and for suggesting a fix!

-1:-- Safari and invalid HTTP/2 headers (Post punchagan)--L0--C0--2026-01-16T16:07:00.000Z

Chris Maiorana: Git changes and commits in Emacs modeline

In a previous post, I mentioned a little bit of modeline hacking to display git changes and today’s commit count. In this article, I thought I would go through those snippets from my modeline code that accomplish this. At the bottom, I’ve also included a standalone minor mode for it.

Basically, this code will show you a format like “250/5” where 250 represents the number of uncommitted changes and 5 represents the number of commits made today.

If you’re interested in topics like these, particularly how you can leverage programs like git for technical and creative writing, I’d highly recommend my downloadable handbooks:

So let’s get into it.

Table of Contents

Git branch display

This section displays the current git branch name in brackets in the modeline. I like seeing the branch name just to make sure I’m in the right place.

(defun csm-modeline--git-branch ()
  "Return the current git branch name, wrapped in brackets.
If not in a git repository, return an empty string."
  (when buffer-file-name
    (let* ((default-directory (file-name-directory buffer-file-name))
           (branch (string-trim
                    (shell-command-to-string "git rev-parse --abbrev-ref HEAD 2>/dev/null"))))
      (if (and branch (not (string-empty-p branch)))
          (format "[%s]" branch)
        ""))))

(defvar-local csm-modeline-directory
  '(:eval
    (when-let ((branch-name (csm-modeline--git-branch)))
      (propertize branch-name 'face 'magit-branch-local)))
  "Mode line construct to display the git branch name.")
(put 'csm-modeline-directory 'risky-local-variable t)
  • csm-modeline--git-branch uses git rev-parse --abbrev-ref HEAD to catch the current branch name.
  • It checks if buffer-file-name exists to ensure we’re in a file-based buffer.
  • Sets default-directory to the file’s directory so git commands run in the correct location.
  • Redirects errors to /dev/null to silently handle cases in which we’re not in a git repository.
  • Formats the branch name in brackets like [master] or [drafting] (my preferred default branch).
  • csm-modeline-directory is the modeline variable that displays the branch with magit-branch-local face styling.
  • The risky-local-variable property is set to t because it evaluates code dynamically.

Git changes and commit count display

This is the main functionality that shows uncommitted changes and today’s commit count.

Caching variables

(defvar csm-modeline-csmchange-cache nil
  "Cache for csmchange command output.")

(defvar csm-modeline-csmchange-last-update 0
  "Timestamp of last csmchange update.")

(defvar csm-modeline-csmchange-update-interval 30
  "Update interval in seconds for csmchange output.")

These variables implement a caching mechanism to avoid running shell commands too frequently:

  • csm-modeline-csmchange-cache stores the last result from the csmchange command
  • csm-modeline-csmchange-last-update tracks when the cache was last refreshed using a Unix timestamp.
  • csm-modeline-csmchange-update-interval sets how often to refresh (30 seconds by default)

This caching is crucial for performance since the modeline updates frequently, but we don’t need to run shell commands every time.

Getting Change Count

(defun csm-modeline--csmchange-output ()
  "Return the output of csmchange command."
  (let ((output (shell-command-to-string "csmchange 2>/dev/null")))
    (string-trim output)))

(defun csm-modeline--cached-csmchange-output ()
  "Return cached csmchange output, updating if necessary."
  (let ((now (float-time)))
    (when (or (null csm-modeline-csmchange-cache)
              (> (- now csm-modeline-csmchange-last-update)
                 csm-modeline-csmchange-update-interval))
      (setq csm-modeline-csmchange-cache (csm-modeline--csmchange-output)
            csm-modeline-csmchange-last-update now)))
  csm-modeline-csmchange-cache)
  • csm-modeline--csmchange-output runs the csmchange command (a custom shell script) and returns the trimmed output.
  • csm-modeline--cached-csmchange-output implements the caching logic:
    • Gets the current time with float-time.
    • Checks if the cache is null (first run) or if enough time has passed since the last update.
    • If an update is needed, it runs csmchange and updates both the cache and timestamp.
    • Returns the cached value.

Here is the csmchange shell script:

#!/bin/bash

# Run the command in the current working directory
git diff --word-diff=porcelain HEAD | grep -e '^+[^+]' -e '^-[^-]' | wc -w

Getting today’s commit Count

(defun csm-modeline--today-commits-count ()
  "Return the number of commits made today."
  (when buffer-file-name
    (let* ((default-directory (file-name-directory buffer-file-name))
           (count (string-trim
                   (shell-command-to-string "git log --since='00:00:00' --oneline --no-merges 2>/dev/null | wc -l"))))
      (if (and count (not (string-empty-p count)))
          count
        "0"))))

This function counts commits made since midnight today:

  • Checks buffer-file-name exists to ensure we’re in a file buffer.
  • Sets default-directory to the file’s directory for correct git context.
  • Uses git log --since='00:00:00' to get commits since midnight.
  • --oneline formats each commit as a single line for easy counting.
  • --no-merges excludes merge commits to count only direct commits.
  • Pipes to wc -l to count the number of lines (commits).
  • Returns “0” if the result is empty or invalid.
  • Errors are silently discarded with 2>/dev/null.

Modeline display variable

(defvar-local csm-modeline-csmchange
  '(:eval
    (when (mode-line-window-selected-p)
      (let ((output (csm-modeline--cached-csmchange-output))
            (commits (csm-modeline--today-commits-count)))
        (when (and output (not (string-empty-p output)))
          (let* ((change-count (string-to-number output))
                 (face (if (>= change-count 250) 'warning 'font-lock-string-face)))
            (concat " "
                    (propertize output 'face face)
                    (when commits
                      (propertize (format "/%s" commits) 'face face))))))))
  "Mode line construct to display csmchange output with today's commit count.")

(put 'csm-modeline-csmchange 'risky-local-variable t)

This is the main modeline variable that brings it all together:

  • Uses :eval to dynamically evaluate the code each time the modeline updates.
  • mode-line-window-selected-p ensures the display only appears in the active window.
  • Retrieves both the cached change count and today’s commit count.
  • Converts the change count to a number to compare against 250.
  • Selects a face: 'warning (usually red/orange), depending on theme, if changes >~ 250, otherwise 'font-lock-string-face (usually green/blue).
  • Formats the output as changes over commits (e.g. “250/5”).
  • Setting risky-local-variable to t allows the dynamic evaluation.

Simplified minor mode

On my GitHub, I have included a self-contained minor mode that implements only the git changes and commit count functionality. I figured this would be easier for individual analysis. If you have any suggestions on how to improve it please let me know. The csmchange shell script logic has been integrated directly into the minor mode, making it fully self-contained without requiring external commands. 

Minor mode features

The simplified minor mode includes:

  1. Built-in Change Counting: The csmchange shell script logic is integrated directly into the package, so no external commands are required (though you can still use an external command if you prefer by setting git-stats-modeline-use-builtin-diff to nil).
  2. Customizable Settings: Users can customize the update interval, warning threshold, whether to use built-in diff counting, and optionally the command to use for external counting.
  3. Same Core Logic: Uses the identical caching and counting logic from the original modeline.
  4. Easy Activation: Simply call (git-stats-modeline-mode 1) to enable or (git-stats-modeline-mode 0) to disable.
  5. Global Minor Mode: Affects all buffers, not just specific ones.
  6. Self-Contained: Can be distributed as a standalone functionality with proper headers and documentation.

How the minor could be used and modified:

;; Load the file
(load-file "/path/to/git-stats-modeline.el")

;; Enable the mode (uses built-in change counting by default)
(git-stats-modeline-mode 1)

;; Optional: customize settings
(setq git-stats-modeline-warning-threshold 300)
(setq git-stats-modeline-update-interval 60)

;; Optional: use external command instead of built-in counting
(setq git-stats-modeline-use-builtin-diff nil)
(setq git-stats-modeline-change-command "csmchange")

The post Git changes and commits in Emacs modeline appeared first on Chris Maiorana.

-1:-- Git changes and commits in Emacs modeline (Post Chris Maiorana)--L0--C0--2026-01-16T10:00:27.000Z

Jack Baty: Fedora/KDE on the Framework laptop

When I was setting up my desktop computer with Linux, I wanted to install Gnome, but I couldn’t get itto work with the Apple Studio Display. I went with KDE instead, and put Gnome on the laptop.

After using both for a couple weeks, it turns out I prefer KDE. This morning, I wiped the Framework and installed KDE. It took me a couple of hours to get to a point where I could do most of the things I normally do (write this post, for example).

I didn’t take detailed notes, but I did list all of the things I’ve done so far. I’m putting it here for safe keeping. I keep threatening to make this into a script, but honestly I’d rather just run through it manually each time.

Install log for Fedora/KDE on the Framework

Jan 16, 2026

  • Configure inverse scrolling
  • Disable tap-to-click
  • Set Caps Lock as Control
  • Log into 1Password
  • Log into Firefox
  • sudo dnf install syncthing
  • Add device to Syncthing from another computer. Share everything
  • sudo dnf install -y stow
  • sudo dnf install -y fzf ripgrep zoxide just
  • curl -sS https://starship.rs/install.sh | sh
  • stow bash
  • stow auth
  • stow ssh
  • sudo dnf install -y pandoc
  • sudo dnf install -y texlive-scheme-full
  • sudo dnf install -y neovim
  • add Start Syncthing to Autostart apps
  • stow pandoc
  • sudo dnf install -y btop
  • sudo dnf install -y fastfetch
  • sudo dnf install -y emacs
  • git clone https://github.com/jamescherti/minimal-emacs.d.git .config/emacs
  • git clone git@github.com:jackbaty/dotemacs.git .config/emacs-mine
  • cp .config/emacs-mine/pre-early-init.el .config/emacs/
  • ln -s ~/Sync/emacs/manual-packages .config/emacs-mine/
  • Install Berkeley Mono font to ./local/share/fonts
  • sudo dnf copr enable dejan/lazygit && sudo dnf install lazygit
  • Install Signal (Flatpak)
  • sudo dnf install aerc
  • stow aerc
  • python3 -m venv maestral-venv
  • python3 -m pip install –upgrade maestral
  • maestral start (then auth with Dropbox)
  • maestral autostart -Y
  • sudo dnf install -y go hugo
✍️ Reply by email
-1:-- Fedora/KDE on the Framework laptop (Post Jack Baty)--L0--C0--2026-01-16T00:00:00.000Z

Alvaro Ramirez: Bending Emacs - Episode 10: agent-shell

I've just uploaded a new Bending Emacs episode:

Bending Emacs Episode 10: agent-shell

You may have seen some of my previous posts on agent-shell, a package I built offering a uniform user experience across a diverse set of agents. In this video, I showcase the main agent-shell features. I had lots to cover, so the video is on the longer side of things.

I've showcased much of the content in previous agent-shell posts, so I'll just share links to those instead:

Hope you enjoyed the video!

Want more videos?

Liked the video? Please let me know. Got feedback? Leave me some comments.

Please go like my video, share with others, and subscribe to my channel.

If there's enough interest, I'll continue making more videos!

-1:-- Bending Emacs - Episode 10: agent-shell (Post Alvaro Ramirez)--L0--C0--2026-01-16T00:00:00.000Z

Rahul Juliato: Eglot with multiple LSP servers per buffer using rassumfrassum

I'll start by admitting something upfront: for a long time, I was very vocal about not recommending Eglot if you were working with a modern web development stack, especially things like React, TypeScript, ESLint, Tailwind, Vue, etc. I even said so publicly in GitHub issues and discussions.

That said, I always liked Eglot.

More than liked, actually. Eglot always felt closer to Emacs itself. So much so that it eventually became part of Emacs core. Its design philosophy: minimalistic, protocol-driven, no magic UI layers is very close to my way to tackle Emacs. In contrast, lsp-mode + lsp-ui behaves much more like a full-blown IDE, and yes, nothing wrong with that approach, you can use lsp-mode without the lsp-ui package, but you know, if I could, I'd rather stay within Emacs provided capabilities.

The real problem for me was simple: I couldn't use all the LSP servers I needed at the same time.

And in modern web development with Eglot, that's not optional anymore 😄.


Intro

It's been a few years now since LSP servers started absorbing responsibilities that once belonged exclusively to linters.

Linting today is faster, more precise, and far more interactive. Diagnostics, quick fixes, refactors, formatting, and even architectural hints are now part of the LSP ecosystem. Servers can recommend actions, explain problems, and react to changes in real time.

Because of that, there's less and less reason to:

• wrap a linter's API into a Flymake backend

• parse CLI output while developing

• reinvent glue code for every tool

The LSP protocol is well documented, standardized, and, at least in theory, every server speaks the same "language".

As time went on, something else became clear: some linters stopped being maintained as standalone tools, while new ones started life as LSP servers first. In some ecosystems, the only supported interface is LSP.

This imposed a brick wall for Eglot usage.

Eglot supports one server per buffer. That was fine when "one server did everything". But today, we often want:

• a language server for semantics and completion

• a linter server for diagnostics and code actions

• a (or some) framework-specific server(s) (Tailwind, Vue, Angular, etc.)


Solutions

For a long time, João (Eglot's maintainer) collected feedback around this limitation. Multiple issues, discussions, and experiments circled around the same idea:

"How do we support multiple LSP servers per buffer without turning Eglot into something it isn't?"

One important constraint was clear: Eglot itself should not be overhauled to manage multiple servers internally.

The proposed solution was elegant: an external multiplexer, a tool that looks like one LSP server to the client, but actually talks to many servers behind the scenes, merging and routing messages appropriately.

I'll admit that having designed hardware-level mux/demuxes in the past made me like this "software" idea.

One implementation which came from these discussions is lspx. I tested it before, and while promising, it wasn't quite mature enough for my daily workflow at the time.

So João did what he had been suggesting for some time.

He stepped in and built it himself.


What is Rassumfrassum

rassumfrassum is an LSP multiplexer, you can check its repository here: https://github.com/joaotavora/rassumfrassum/

From the client's perspective (Eglot, Neovim, anything), it behaves like a single stdio LSP server. Internally, it spawns and manages multiple real LSP servers, routing requests and merging responses.

You start it like this:

rass -- server-a [params-a] -- server-b [params-b] -- server-c [params-c]

Or, using presets, like:

rass python

Behind the scenes, Rassumfrassum:

• forwards requests to all relevant servers

• aggregates diagnostics

• merges code actions and completions

• handles timing, delays, and late responses

• optionally streams diagnostics incrementally

All of this is implemented in Python, with a clear separation between:

• JSON-RPC plumbing

• LSP semantics

• server-specific logic

This is also where Rassumfrassum introduces a non-standard but optional streaming diagnostics extension, which allows multiple diagnostic sources to coexist without stomping on each other.

See it in action from João's screencapture:

rassumfrassum


Bringing it to my React Web Dev workflow

My day job often involves a lot of React web development, from different repo sizes and ages, and this is where Rassumfrassum becomes a killer feature for me.

Let's take an example, a typical modern React stack needs, at minimum (as suggested by NextJS framework I'll present in a example below):

typescript-language-server (types, navigation, refactors)

ESLint (diagnostics, fixes)

tailwindcss-language-server (class completion, validation)

Before this, with Eglot, you had to choose one.

To properly test this, I created a minimal but realistic example repository you can clone:

# **demo_react_ts_eslint_tailwind_app_for_lsp_debug**
git clone https://github.com/LionyxML/demo_react_ts_eslint_tailwind_app_for_lsp_debug

It's basically this so-called "default" modern React setup: TypeScript, ESLint, Tailwind.

To make this post not too long, I'm assuming the reader knows about npm, pnpm and other tools related to the javascript world.

Basically you need to clone the repo and install the app with:

pnpm install

And make sure you have the needed LSP servers installed, I did it with:

pnpm install -g typescript-language-server typescript @tailwindcss/language-server eslint-lsp

Note: Yep, I know about vscode-eslint-language-server being newer, but still I had problems with it, eslint-lsp worked, and I'm happy.

After fixing a broken Tailwind server installation on my side (user error 😄), this setup worked flawlessly with:

rass -- typescript-language-server --stdio \
	 -- eslint-lsp --stdio \
	 -- tailwindcss-language-server --stdio

You don't need to run this in your terminal, instead, visit your project file and fire up Eglot using:

C-u M-x eglot RET
rass -- typescript-language-server --stdio -- eslint-lsp --stdio -- tailwindcss-language-server --stdio RET

This project README provides you with directions on what is expected from each LSP server to "see". The only relevant file is app/page.tsx.

Well, I did all that and suddenly, everything was there:

Flymake buffer listing diagnostics from all servers

Code completion from both TypeScript and Tailwind

Code actions

Fast, responsive, and clean. Here are some other screenshots:

• the example React code with Flymake marking diagnostics on margin and in-buffer: demo-02

• Flymake buffer showing messages from typescript, eslint and tailwind: demo-03

• Eldoc showing typescript + eslint warnings while also providing type description: demo-05

• Typescript completion: demo-06

• Tailwind completion: demo-07

• Code actions: demo-04

It feels fast. It feels correct. And most importantly: it feels like Emacs, not an IDE pretending to be Emacs.

And of course, if everything works for you when firing up Eglot manually, you can add something like this to your config:

(with-eval-after-load 'eglot
  (add-to-list
   'eglot-server-programs
   '((tsx-ts-mode typescript-ts-mode)
	 . ("rass"
		"--"
		"typescript-language-server" "--stdio"
		"--"
		"eslint-lsp" "--stdio"
		"--"
		"tailwindcss-language-server" "--stdio"))))

A small debugging tip: you can check eglot connections with M-x eglot-list-connections RET and be sure it is running your custom invocation command.


Conclusion

I want to sincerely thank João Távora for his continuous work for the Emacs community over the years.

Rassumfrassum solves a long-standing, real-world problem in a way that respects both:

• the LSP ecosystem

• Eglot's design philosophy

This is still a young project, and there will be bugs. That's expected. But the foundation is solid, the approach is elegant, and the impact is huge.

If you were, like me, holding back on Eglot for modern web development this changes everything.

Please try it, report issues, and support the project. This is not just a win for Eglot, it's a win for the entire LSP ecosystem.

-1:-- Eglot with multiple LSP servers per buffer using rassumfrassum (Post Rahul Juliato)--L0--C0--2026-01-15T20:44:55.000Z

noa ks: The eternal struggle against org's overenthusiastic keybindings

Update: Ihor Radchenko emailed me to point out that org’s shift bindings are actually older than shift-select-mode, and as such “confusing behaviour” refers to changing the workflow of people who have already got used to org’s keys. Please keep that in mind when reading!

I don’t think i hide the fact that i like consistency, and one of the nice things about emacs is that whatever i am doing, i have the same keybindings everywhere. Everywhere, that is, except for in org mode, one of emacs’s flagship applications, which has absolutely no qualms about rebinding keys willy nilly. Luckily, org provides the variable org-support-shift-select, set to nil by default which, when non-nil, makes “shift-cursor commands select text when possible”. That’s really nice. I’m glad that’s an option, however i’m really unconvinced by the documentation this option provides, starting with this:

The default of this variable is nil, to avoid confusing behavior.

Now i’m no expert, but replacing behaviour that works everywhere else in emacs with special behaviour in particular contexts does not to me scream “not confusing”. The most outrageous part though, comes at the very bottom of the docstring:

However, when the cursor is on a timestamp, shift-cursor commands will still edit the time stamp - this is just too good to give up.

No! Avoiding this kind of making decisions on my behalf is exactly why i’m using emacs in the first place. But it’s not a problem, because i am using emacs in the first place, and therefore i can skip right over options telling me what i should consider too good to give up, and let my environment know exactly what i think is too good to give up:

(use-package org
          :bind (:map org-mode-map
          ("S-<up>" . nil)
          ("S-<down>" . nil)
          ("S-<left>" . nil)
          ("S-<right>" . nil)
          ("C-S-<up>" . nil)
          ("C-S-<down>" . nil)
          ("C-S-<left>" . nil)
          ("C-S-<right>" . nil)
          ("S-<return>" . nil)))
        

For me, the answer is consistency, everywhere. Thank you, emacs!

-1:-- The eternal struggle against org's overenthusiastic keybindings (Post noa ks)--L0--C0--2026-01-15T16:00:00.000Z

Irreal: Making Agenda Items Into Appointments

Every Org mode user knows that you can add tasks to your Org agenda with a SCHEDULED or DEADLINE keyword. That will cause your agenda to warn you ahead of time of upcoming tasks requiring your attention. Emacs also has an appointment system—tied to the diary—that will give you an active warning just before you need to act.

It would be nice if you could have those agenda tasks somehow be promoted to appointments so that you’d get a timely warning when the task is due to be acted on. It’s Emacs so of course you can. The idea is pretty old. Here’s Sacha Chua writing about it before it became part of Emacs in 2007. The answer is to use org-agenda-to-appt. It will scan your agenda files and set appointments for each entry that has a SCHEDULED or DEADLINE keyword.

The problem is automating this. You don’t want to have to call org-agenda-to-appt every time you add a task with an action date. Over at Dave’s Blog, Dave has a solution. It amounts to running org-agenda-to-appt every hour to pick up new potential appointments. See Dave’s post for the details.

What you’d really like is to be able to advise the agenda functions to add the appointment when you add a new agenda item. That’s non-trivial but probably doable. In the mean time, Dave’s solution is a reasonable approach if you’d like to try automatically promoting your agenda items to appointments.

-1:-- Making Agenda Items Into Appointments (Post Irreal)--L0--C0--2026-01-15T15:48:11.000Z

Sacha Chua: Visualizing and managing Pipewire audio graphs from Emacs

I want to be able to record, stream, screen share, and do speech recognition, possibly all at the same time. If I just try having those processes read directly from my microphone, I find that the audio skips. I'm on Linux, so it turns out that I can set up Pipewire with a virtual audio cable (loopback device) connecting my microphone to a virtual output (null sink) with some latency (100ms seems good) so that multiple applications listening to the null sink can get the audio packets smoothly.

I was getting a little confused connecting things to other things, though. qpwgraph was helpful for starting to understand how everything was actually connected to each other, and also for manually changing the connections on the fly.

2026-01-13_10-06-59.png
Figure 1: qpwgraph screenshot

Like with other graphical applications, I found myself wondering: could I do this in Emacs instead? I wanted to just focus on a small set of the nodes. For example, I didn't need all of the lines connecting to the volume control apps. I also wanted the ability to focus on whichever nodes were connected to my microphone.

Unsurprisingly, there is a pipewire package in MELPA.

2026-01-14_16-39-37.png
Figure 2: Screenshot of M-x pipewire from the pipewire package

I want to see and manage the connections between devices, though, so I started working on sachac/epwgraph: Emacs Pipewire graph visualization. This is what epwgraph-show looks like with everything in it:

2026-01-14_16-50-39.png
Figure 3: epwgraph-show

Let's call it with C-u, which prompts for a regexp of nodes to focus on and another regexp for nodes to exclude. Then I can ignore the volume control:

2026-01-14_16-51-16.png
Figure 4: Ignoring the volume control

I can focus on just the things that are connected to my microphone:

2026-01-14_16-51-56.png
Figure 5: Focusing on a regular expression

This also lets me disconnect things with d (epwgraph-disconnect-logical-nodes):

2026-01-14_16-52-35.png
Figure 6: Disconnecting a link

and connect them with c (epwgraph-connect-logical-nodes).

2026-01-14_16-52-57.png
Figure 7: Connecting links

I don't have a fancy 5.1 sound systems, so the logic for connecting nodes just maps L and R if possible.

Most of the time I just care about the logical devices instead of the specific left and right channels, but I can toggle the display with t so that I can see specific ports:

2026-01-14_17-17-34.png
Figure 8: Showing specific ports

and I can use C and D to work with specific ports as well.

2026-01-14_18-10-55.png
Figure 9: Connecting specific ports

I usually just want to quickly rewire a node so that it gets its input from a specified device, which I can do with i (epwgraph-rewire-inputs-for-logical-node).

output-2026-01-14-17:30:18.gif
Figure 10: Animated GIF showing how to change the input for a node.

I think this will help me stay sane when I try to scale up my audio configuration to having four or five web conferences going on at the same time, possibly with streaming speech recognition.

Ideas for next steps:

  • I want to be able to set the left/right balance of audio, probably using pactl set-sink-volume <index> left% right%
  • I'd love to be able to click on the graph in order to work with it, like dragging from one box to another in order to create a connection, right-drag to disconnect, or shift-drag to rewire the inputs.

In case this is useful for anyone else:

sachac/epwgraph: Emacs Pipewire graph visualization

View org source for this post

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

-1:-- Visualizing and managing Pipewire audio graphs from Emacs (Post Sacha Chua)--L0--C0--2026-01-14T22:39:20.000Z

Sacha Chua: Emacs Lisp: Editing one file twice at the same time

@HaraldKi@nrw.social said:

Emacs can do everything. Except the most simple thing ever as I learned after 40 years in which I never needed it: Edit one file twice at the same time.

I can open a new Emacs "window" and re-open the file. But Emacs notices and this and shows the file's buffer in the new window, not a new buffer.

But why? Well, when editing and SVG file, you can switch between the XML and the rendered image with C-c C-c, but I would like to see the XML and the rendered next to each other.😀

You might think this is easy, just use M-x clone-indirect-buffer-other-window. But image-mode adds a wrinkle. It uses text properties to display the image, so even if you have two views of the same buffer thanks to clone-indirect-buffer, C-c C-c will toggle both of them. If we want to edit a file as both text and an SVG at the same time, we need to actually have two separate file buffers.

I started off by looking at how find-file works. From there, I went to find-file-noselect. Normally, find-file-no-select reuses any existing buffers visiting the same file. If it doesn't find any, it calls find-file-noselect-1. That lets me write this short function to jump straight to that step.

(defun my-find-file-always (filename &optional buffer-name)
  (interactive (list (read-file-name "File: ")))
  (setq buffer-name (or (create-file-buffer filename)))
  (let* ((truename (abbreviate-file-name (file-truename filename)))
         (attributes (file-attributes truename))
         (number (file-attribute-file-identifier attributes)))
    (with-current-buffer
        (find-file-noselect-1
         (get-buffer-create buffer-name)
         truename
         t nil truename number)
      (when (called-interactively-p 'any)
        (switch-to-buffer (current-buffer)))
      (current-buffer))))

(defun my-clone-file-other-window ()
  (interactive)
  (display-buffer-other-window (my-find-file-always (buffer-file-name))))

This code unconditionally opens a buffer visiting a file, so you could have multiple buffers, looking at the same file independently. With global-auto-revert-mode, editing the file in one buffer and saving it will result in changes in the other.

I sometimes play around with SVGs, and it might be helpful to be able to experiment with the source code of the SVG while seeing the changes refreshed automatically.

I really like how in Emacs, you can follow the trail of the functions to find out how they actually work.

Screencast demonstrating my-find-file-always

Transcript

00:00:00 The problem: clone-indirect-buffer-other-window and image-mode
@HaraldKi@nrw.social said, "Emacs can do everything except the most simple thing ever, as I learned after 40 years in which I never needed it: edit one file twice at the same time." You might think this is easy, just use M-x clone-indirect-buffer-other-window, but image mode adds a wrinkle. So let's show you how that works. I've got my test SVG here. We can say clone-indirect-buffer-other-window. But if I use C-c C-c, you'll notice that both of the windows change. That's because image mode uses text properties instead of some other kind of display. I mean, it's the same buffer that's being reused for the clone. So that doesn't work.
00:00:48 A quick tour of find-file
What I did was I looked at how find-file works. And then from there, I went to find-file-noselect. So this is find-file over here. If you look at the source code, you'll see how it uses find-file... It's a very short function, actually. It uses find-file-noselect. And find-file-noselect reuses a buffer if it can. Let's show you where we're looking for this. Ah, yes. So here's another buffer here. And what we want to do is we want to open a new file buffer no matter what. The way that find-file-noselect actually works is it calls this find-file-noselect1. And by taking a look at how it figured out the raw file and the true name and the number to send to it, I was able to write this short function, my-find-file-always, and a my-clone-file-other-window.
00:01:46 Demonstration of my-find-file-always
So if I say my-find-file-always, then it will always open that file, even if it's already open elsewhere.
00:01:57 Cloning it into the other window
Let's show you how it works when I clone it in the other window. All right, so if I switch this one to text mode, I can make changes to it. More stuff goes here. And as you can see, that added this over here. I have global-auto-revert mode on, so it just refreshes automatically. So yeah, that's this function.

View org source for this post

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

-1:-- Emacs Lisp: Editing one file twice at the same time (Post Sacha Chua)--L0--C0--2026-01-14T19:25:06.000Z

Chris Maiorana: The 10-Commit Rule: how git version control can improve writing quality

An often overlooked part of the productivity equation is knowing when to stop. Sometimes it feels good to keep adding and keep going, but you don’t want to burnout too quickly.

As I mentioned in my Git For Writers book, I use commits as a way of measuring “thought units.” I estimate that I need about 10-12 commits per thousands words of finished prose to know I’ve really put sufficient thought into a piece of writing. A commit will generally encompass ~250-300 words changed.

I have a counter in my Emacs modeline that tells me how many words have changed and how many commits I’ve made today. Once I start to creep up to 250 or 300 words added or deleted, that’s a good sign that I should stop, review what I’ve done, and log a commit.

The modeline can show all kinds of useful data, but you don’t want to clutter it up.

“Drafting” is the name of the current branch, the first number is the amount of current changes made on the project, and the number after the slash is how many commits have been made today.

This has been incredibly helpful for a few reasons. It’s given me sensible metrics I can hit on a regular basis. This makes planning out effort much easier.

In the past I’ve tended to get excited early in a project and work too fast and aggressively and burn out, take some time off, and come back later to finish. Having more sensible targets early on, and a better scope of what I want to accomplish, helps me stay on track without burning out.

Likewise, this helps me make sure I’ve put enough thought into something before sending out into the world. You don’t want to send out a cake half-baked. Seeing that I’ve put a lot of commits on something is an easy way of gauging the effort I’ve expended.

The process emphasizes strategy. By stopping periodically to see what I’ve done, I can approach the work with more sensitivity. I can see where I’m putting effort and where I might need more or less.

The post The 10-Commit Rule: how git version control can improve writing quality appeared first on Chris Maiorana.

-1:-- The 10-Commit Rule: how git version control can improve writing quality (Post Chris Maiorana)--L0--C0--2026-01-14T11:25:39.000Z

Irreal: Unfill

I saw this post from mbork on paragraph filling. It’s a nice post and I’ll probably write about it later but today I want write about something else he mentioned in his post: unfill.el. It’s from Steve Purcell and has been around for a couple of years but I somehow missed it.

Most of my Emacs buffers are set to wrap text, which is usually what you want to do but for writing buffers, such as my blog posts, I use visual line mode and don’t want any filling. More often than you’d think I would, I find myself writing a blog post or some other prose and discover that I’m filling text lines.

My usual solution to this is to let the line length to a large value and call fill-paragraph. That’s not very hard but it’s a pain and I should have automated it long ago but my laziness kicked in and I never did. Happily, Purcell has done this for me. It does the same thing I did manually but with a single key press. Even better, he provides the unfill=toggle function. If you call it once, it fills the paragraph. If you call it again, it unfills the paragraph. That’s nice because you can bind unfill-toggle to Meta+q, or whatever you usually bind the fill command to, and get both commands for the price of one. In the mean time, I’ve installed unfill.el and bound Meta+q to unfill-toggle. One more tiny nub sanded down.

Update [2026-01-17 Sat 15:57]: unfill.org → unfill.el

-1:-- Unfill (Post Irreal)--L0--C0--2026-01-13T15:36:23.000Z

Dave's blog: Tmux and the mouse

I use tmux for just a few things, particular when connecting to a remote system. But otherwise, I don’t use it for local terminals, as I can just open up another terminal in Wayland.

I have a Kensington trackball with a big scroll wheel. I’ve gotten very used to using that scroll wheel for scrolling in terminals, Emacs, my web browser, etc. But one place it didn’t work was in tmux.

But today I finally went searching around for how to turn on mouse support. Something I read prompted me to think “Oh yeah, how do I do that?” It turns out tmux does support the mouse, but not by default. So you have to enable the support in your tmux configuration file. I added

set -g mouse on

to $HOME/.config/tmux/tmux.conf, restarted tmux, and boom, I have mouse wheel scrolling like most other applications I use.

-1:-- Tmux and the mouse (Post Dave's blog)--L0--C0--2026-01-13T00:00:00.000Z

Chris Maiorana: Plugging Python into Git

I recently demoed a python script on my YouTube channel for analyzing git commits and extracting writing stats. This script is ideal for daily writing output analysis and planning.

As promised, I wanted to drop the full text I was using for the demonstration. You can find it here.

As always, this stuff is just for educational purposes and not offered with any kind of guarantee that will run properly on your own system. So, as always, run at your own risk. The script does not write any information into the file system or require any special privileges. It merely looks back at git commits made over the current day and provides some analysis.

What I particularly like:

  • The “edit ratio” feature: this nifty little tool tells you how much you wrote (added) versus what you edited (deleted). I intended for this feature to give the writer a clearer view of their habits. Is he writing more than editing, or editing more than writing?
  • The writing streak: keep a count of how many days consecutive days you’ve made commits on your writing project.
  • Commit and note messages: beyond your commit messages, you can leave extended notes to yourself about what you did in that commit.

These procedures go along nicely the theory I write about at length in my Git For Writers handbook, which I would encourage you to check out if you are high-performer writer interested in optimizing your writing output. Thanks for reading. See you next time.

The post Plugging Python into Git appeared first on Chris Maiorana.

-1:-- Plugging Python into Git (Post Chris Maiorana)--L0--C0--2026-01-12T20:38:00.000Z

Sacha Chua: 2026-01-12 Emacs news

If you want to review packages before upgrading them, check out the new package.el feature for reviewing diffs (Reddit, Irreal).

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 comment on Mastodon or e-mail me at sacha@sachachua.com.

-1:-- 2026-01-12 Emacs news (Post Sacha Chua)--L0--C0--2026-01-12T19:32:14.000Z

Marcin Borkowski: Making fill-paragraph more flexible

I wrote recently about fill-paragraph-semlf and how I moved to it entirely with M-q. It quickly turned out that it wasn’t the best idea. Sometimes I want to be able to fill the paragraph in the old way, too. I figured that the best way to do it is to make M-q do that when pressed for the second time.
-1:-- Making fill-paragraph more flexible (Post Marcin Borkowski)--L0--C0--2026-01-12T18:17:31.000Z

Irreal: The Emacs OS: The Unix Way Vs. The Emacs Way

Chris Maiorana has decided to take the old joke about Emacs being an operating seriously. Or at least at face value. He has planned a series of posts where he compares the Unix way of doing things to the Emacs way.

The first post in the series compares directory listings. That’s a fundamental function for any operating system, of course, so it makes sense—if you’re comping operating systems—to see how they handle that task.

Maiorana’s answer is to look at the various outputs from the Unix ls command and compare them to the results from Dired. There’s not much to compare, of course, because the Dired listings are generated from the underlying ls command. The difference is how you handle options. In Unix this is done through command line options. In Emacs, you change the Dired listing after it’s first displayed with buffer specific commands as usual in Emacs.

The real difference is in the power that Dired provides. You can’t do anything that you couldn’t do in Unix but with Dired it’s all one command. You think of the listing as just another Emacs buffer that you operate on in the usual ways or perhaps bring bespoke commands to bear.

I don’t really believe in the Emacs as OS paradigm—although I sometimes pretend I do. A more accurate description is that those of us who live in Emacs tend to treat it as if it were a shell. With Emacs, there is seldom any need to drop into an actual shell to get things done and even if you do, Emacs can provide you with several including it own eshell.

I’ll be interested to see Maiorana’s subsequent posts on the subject. I’m interested in exploring the extent to which Emacs can be claimed to be an operating system.

-1:-- The Emacs OS: The Unix Way Vs. The Emacs Way (Post Irreal)--L0--C0--2026-01-12T15:04:38.000Z

Irreal: A New Package.el Feature

One of the problems with the ELPA package manager is that you never know what you’re getting when you upgrade. The package manager tells you what packages can be upgraded and you can choose to upgrade or not but you don’t know what’s changed.

Happily, a new feature has just been committed. Now you can examine a diff between the old and new versions to get an idea of what has changed. It’s not ideal if you can’t read Elisp but at least those who do read it can get an idea of how the new version differs from the previous.

It’s not a big thing, I suppose, but it does show how Emacs is continuing to improve and make its users’ lives better. This new capability probably won’t be available until the next release but it’s something to look forward to.

-1:-- A New Package.el Feature (Post Irreal)--L0--C0--2026-01-11T15:46:16.000Z

Chris Maiorana: The Emacs Way: Listing Directory Contents

You’ve often heard it said that Emacs is more of an operating system than a text editor. While this was often meant as a joke, I’ve seen more and more Emacs users leaning into this distinction.

In that spirit, I wanted to do some posts comparing the traditional UNIXy way of doing things with how we can accomplish the same tasks in our Emacs “operating system.”

This week, we’re going to take a look at listing directory contents. At some point or another we all need to list out the contents of a directory.

The UNIXy way of doing it is quite simple and elegant. We have the nice ls command.

As the man pages describe ls:

List information about the FILEs (the current directory by default).

In Emacs, however, we have a full-fledged file manager in dired, the directory editor.
So we can view the contents of a directory but also create, move, rename files, and much much more.

Let’s break it down.

UNIXy Way

ls
Simple list
ls -l
Long format with permissions, owner, size, date
ls -a
Show hidden files (starting with .)
ls -la
Long format + hidden files (my favorite)
ls -lh
Long format with human-readable sizes (KB, MB, GB)

Example output:

$ ls -lh
total 24K
drwxr-xr-x 2 user user 4.0K Jan  9 10:30 documents
-rw-r--r-- 1 user user 1.2K Jan  8 14:22 notes.txt
-rwxr-xr-x 1 user user 8.5K Jan  7 09:15 script.sh
drwxr-xr-x 3 user user 4.0K Jan  6 16:45 projects

Emacs Way

  • C-x d (dired) – Opens directory editor
  • C-u C-x d – Opens dired with custom switches (e.g., “-la”)
  • In dired: s toggles between name/date sorting
  • In dired: ( toggles detailed listing

Example dired buffer:

/home/user:
drwxr-xr-x  2 user user 4096 Jan  9 10:30 documents
-rw-r--r--  1 user user 1234 Jan  8 14:22 notes.txt
-rwxr-xr-x  1 user user 8765 Jan  7 09:15 script.sh
drwxr-xr-x  3 user user 4096 Jan  6 16:45 projects

In dired, you can mark files, operate on multiple files, and navigate using standard Emacs keybindings.

Look out for more of these types of “Emacs way” posts as I’ve got a whole bunch planned.

Thanks, as always, for reading. If you’d like to support my work, please consider subscribing to my free newsletter or purchasing one of my books.

The post The Emacs Way: Listing Directory Contents appeared first on Chris Maiorana.

-1:-- The Emacs Way: Listing Directory Contents (Post Chris Maiorana)--L0--C0--2026-01-11T12:31:09.000Z

Mike Olson: Announcing eglot-python-preset

Contents

Introduction

I’m happy to announce the release of eglot-python-preset, a new Emacs package that simplifies Python LSP configuration with Eglot. It’s now available on MELPA.

The package was born from a discussion on r/emacs about using Emacs for Python development with uv and basedpyright. The original poster had a common pain point: they were writing standalone Python scripts with PEP-723 inline dependencies, using uv run script.py to execute them. Everything worked fine from the command line, but their LSP (basedpyright) couldn’t resolve the imports because it had no awareness of uv’s cached environments.

This is a gap in the tooling. uv manages isolated environments for PEP-723 scripts automatically, but there was no bridge between those environments and the editor’s language server. You’d get reportMissingImports errors for packages that were clearly installed and working at runtime.

What eglot-python-preset Does

The package handles the glue between uv, PEP-723 scripts, and your LSP server. When you open a Python file containing PEP-723 metadata:

  1. It detects the script metadata automatically
  2. It locates the cached uv environment for that script (or prompts you to create one)
  3. It configures the LSP server to use that environment for type checking

For standard Python projects with pyproject.toml or requirements.txt, the package handles project root detection so Eglot starts from the right directory.

Basic Setup

Installation from MELPA:

(use-package eglot-python-preset
  :ensure t
  :after eglot
  :custom
  (eglot-python-preset-lsp-server 'ty) ; or 'basedpyright
  :config
  (eglot-python-preset-setup))

The package supports both ty (Astral’s new Rust-based type checker) and basedpyright. See my earlier post on ty for why you might want to try ty — it’s dramatically faster than the alternatives.

Key Commands

  • M-x eglot-python-preset-sync-environment - Sync dependencies for a PEP-723 script using uv sync --script, then restart Eglot. Use this after adding or modifying dependencies in your script’s metadata block.

  • M-x eglot-python-preset-remove-environment - Remove the cached environment to force a clean reinstall.

  • M-x eglot-python-preset-run-script - Run the current script using uv run in a compilation buffer.

Future Directions

There are two potential follow-ups I’m considering:

Autodetecting tools: Currently you choose between ty and basedpyright manually. It would be useful to detect the appropriate LSP server automatically based on project configuration or installed tools.

rass support: The rassumfrassum project (an LSP multiplexer by Eglot’s author) could integrate with eglot-python-preset to run multiple servers like ty and Ruff simultaneously. rass might eventually handle autodetection itself, or it could be done on the elisp side. Either way, better integration with rass may be worth exploring.

Links

-1:-- Announcing eglot-python-preset (Post Mike Olson)--L0--C0--2026-01-11T00:00:00.000Z

Irreal: Running Emacs As A Service

Matthias over at Ahoi Blog makes an observation that I’ve also made many times: Discussion about Emacs startup times ends as soon as you run Emacs as a daemon. The startup is instantaneous and if you prefer a Vim-like way of operating where you start and stop Emacs each time you need it you can have it. Of course, Emacs doesn’t really stop. It’s still running as a daemon and will pop up a new frame for you whenever you need one.

With this model, the Emacs startup time is absorbed into the system boot time, which presumably happens rarely. The question is, how to get the daemon started. If you want to have it happen at boot time, the exact method depends on your operating system.

Matthias shows how to do this in Linux. It’s a little finicky because of systemd but really just amounts to making an entry in a configuration file. You can see the details in his post. The other issue is how to start, stop, and interact with the Emacs daemon when it’s running. Matthias has a simple script that automates all that so that you don’t worry about the intricacies of systemctl and journalctl.

The other possibility is to simply never exit Emacs. That’s what I do. I run it in its own workspace so I don’t have to worry about hiding it when it’s not in use. I also start server mode so that I can instantly pop up a frame (either GUI or terminal) if I want to for some reason. I really do think this is the best solution but others obviously disagree. If you like to bring up your editor when you need it, quit when you don’t, and your editor is Emacs, you should take a look at Matthias’ post. It will tell you how to run Emacs the way you want to and you’ll never have to think about startup time again.

-1:-- Running Emacs As A Service (Post Irreal)--L0--C0--2026-01-10T15:44:46.000Z

James Dyer: A single function ripgrep alternative to rgrep

For years, rgrep has been the go-to solution for searching codebases in Emacs. It’s built-in, reliable, and works everywhere. But it’s slow on large projects and uses the aging find and grep commands.

Packages like deadgrep and rg.el provide ripgrep integration, and for years I used deadgrep and really liked it. But what if you could get ripgrep’s speed with just a single function you paste into your config?

This post introduces a ~100 line defun that replaces rgrep, no packages, no dependencies, just pure Elisp. It’s fast, asynchronous, works offline, and mimics rgrep’s familiar interface so it can leverage grep-mode

So, why not just use rgrep?

I think that rgrep has three main limitations:

Firstly, speed. On a project with 10,000+ files, rgrep can take 15-30 seconds. Ripgrep completes the same search in under a second.

Secondly, file ignoring, rgrep requires manually configuring grep-find-ignored-directories or grep-find-ignored-files, I had the following typical configuration for rgrep, but it wasn’t as flexible as I would like it to be:

(eval-after-load 'grep
 '(progn
 (dolist (dir '("nas" ".cache" "cache" "elpa" "chromium" ".local/share" "syncthing" ".mozilla" ".local/lib" "Games"))
 (push dir grep-find-ignored-directories))
 (dolist (file '(".cache" "*cache*" "*.iso" "*.xmp" "*.jpg" "*.mp4"))
 (push file grep-find-ignored-files))
 ))

Ripgrep automatically respects an .ignore file. Just create an .ignore file in your project root and list patterns to exclude, this is just a simple text file, universally applied across all searches and any changes can be easily applied.

Thirdly, modern features. Ripgrep includes smart-case search, better regex support, and automatic binary file detection. Of course, there is a context that can be displayed around the found line, but in order to get ripgrep to work with grep-mode, this is not really doable, and it’s not something I need anyway.


Here is the complete ripgrep implementation that you can paste directly into your init.el:

(defun my/grep (search-term &optional directory glob)
 "Run ripgrep (rg) with SEARCH-TERM and optionally DIRECTORY and GLOB.
If ripgrep is unavailable, fall back to Emacs's rgrep command. Highlights SEARCH-TERM in results.
By default, only the SEARCH-TERM needs to be provided. If called with a
universal argument, DIRECTORY and GLOB are prompted for as well."
 (interactive
 (let* ((univ-arg current-prefix-arg)
 (default-search-term
 (cond
 ((use-region-p)
 (buffer-substring-no-properties (region-beginning) (region-end)))
 ((thing-at-point 'symbol t))
 ((thing-at-point 'word t))
 (t ""))))
 (list
 (read-string (if (string-empty-p default-search-term)
 "Search for: "
 (format "Search for (default `%s`): " default-search-term))
 nil nil default-search-term)
 (when univ-arg (read-directory-name "Directory: "))
 (when univ-arg (read-string "File pattern (glob, default: ): " nil nil "")))))
 (let* ((directory (expand-file-name (or directory default-directory)))
 (glob (or glob ""))
 (buffer-name "*grep*"))
 (if (executable-find "rg")
 (let ((buffer (get-buffer-create buffer-name)))
 (with-current-buffer buffer
 (setq default-directory directory)
 (let ((inhibit-read-only t))
 (erase-buffer)
 (insert (format "-*- mode: grep; default-directory: \"%s\" -*-\n\n" directory))
 (if (not (string= "" glob))
 (insert (format "[o] Glob: %s\n\n" glob)))
 (insert "Searching...\n\n"))
 (grep-mode)
 (setq-local my/grep-search-term search-term)
 (setq-local my/grep-directory directory)
 (setq-local my/grep-glob glob))

 (pop-to-buffer buffer)
 (goto-char (point-min))

 (make-process
 :name "ripgrep"
 :buffer buffer
 :command `("rg" "--color=never" "--max-columns=500"
 "--column" "--line-number" "--no-heading"
 "--smart-case" "-e" ,search-term
 "--glob" ,glob ,directory)
 :filter (lambda (proc string)
 (when (buffer-live-p (process-buffer proc))
 (with-current-buffer (process-buffer proc)
 (let ((inhibit-read-only t)
 (moving (= (point) (process-mark proc))))
 (setq string (replace-regexp-in-string "[\r\0\x01-\x08\x0B-\x0C\x0E-\x1F]" "" string))
 ;; Replace full directory path with ./ in the incoming output
 (setq string (replace-regexp-in-string
 (concat "^" (regexp-quote directory))
 "./"
 string))
 (save-excursion
 (goto-char (process-mark proc))
 (insert string)
 (set-marker (process-mark proc) (point)))
 (if moving (goto-char (process-mark proc)))))))
 :sentinel
 (lambda (proc _event)
 (when (memq (process-status proc) '(exit signal))
 (with-current-buffer (process-buffer proc)
 (let ((inhibit-read-only t))
 ;; Remove "Searching..." line
 (goto-char (point-min))
 (while (re-search-forward "Searching\\.\\.\\.\n\n" nil t)
 (replace-match "" nil t))

 ;; Clean up the output - replace full paths with ./
 (goto-char (point-min))
 (forward-line 3)
 (let ((start-pos (point)))
 (while (re-search-forward (concat "^" (regexp-quote directory)) nil t)
 (replace-match "./" t t))

 ;; Check if any results were found
 (goto-char start-pos)
 (when (= (point) (point-max))
 (insert "No results found.\n")))

 (goto-char (point-max))
 (insert "\nRipgrep finished\n")

 ;; Highlight search terms using grep's match face
 (goto-char (point-min))
 (forward-line 3)
 (save-excursion
 (while (re-search-forward (regexp-quote search-term) nil t)
 (put-text-property (match-beginning 0) (match-end 0)
 'face 'match)
 (put-text-property (match-beginning 0) (match-end 0)
 'font-lock-face 'match))))

 ;; Set up keybindings
 (local-set-key (kbd "D")
 (lambda ()
 (interactive)
 (my/grep my/grep-search-term
 (read-directory-name "New search directory: ")
 my/grep-glob)))
 (local-set-key (kbd "S")
 (lambda ()
 (interactive)
 (my/grep (read-string "New search term: "
 nil nil my/grep-search-term)
 my/grep-directory
 my/grep-glob)))
 (local-set-key (kbd "o")
 (lambda ()
 (interactive)
 (my/grep my/grep-search-term
 my/grep-directory
 (read-string "New glob: "))))
 (local-set-key (kbd "g")
 (lambda ()
 (interactive)
 (my/grep my/grep-search-term my/grep-directory my/grep-glob)))

 (goto-char (point-min))
 (message "ripgrep finished."))))
 )
 (message "ripgrep started..."))
 ;; Fallback to rgrep
 (progn
 (setq default-directory directory)
 (message (format "%s : %s : %s" search-term glob directory))
 (rgrep search-term (if (string= "" glob) "*" glob) directory)))))

That’s it. ~100 lines. No dependencies. No packages to manage! (well except ripgrep of course)

Now that I have complete control over this function, I have added further improvements over rgrep, inspired by deadgrep

  • S - New search term
  • D - New directory
  • o - New glob pattern
  • g - Re-run current search

and a universal argument can be passed through to set these up on the initial grep

I have tried to make the output as similar as possible to rgrep, to be compatible with grep-mode and for familiarity, so it will be something like:

-*- mode: grep; default-directory: "~/project/" -*-
[o] Glob: *.el
./init.el:42:10:(defun my-function ()
./config.el:156:5: (my-function)
./helpers.el:89:12:;; Helper for my-function
Ripgrep finished

and if a glob is applied it will display the glob pattern.

Its perfect for offline environments, and yes, I’m banging on about this again!, no network, no package manager, no dependencies (except ripgrep of course!)

-1:-- A single function ripgrep alternative to rgrep (Post James Dyer)--L0--C0--2026-01-09T09:43:00.000Z

Protesilaos Stavrou: Emacs: my ‘oxford-calendar’ package

The oxford-calendar is a small package that is of interest to students, academics, and staff of the University of Oxford. It augments the M-x calendar buffer with indicators that show the applicable term (Michaelmas, Hilary, Trinity) and week number.

Oxford calendar for Emacs

To show the indicators, enable the oxford-calendar-mode. By default, it includes the extra weeks 0 and 9 at the boundaries of each term. The idea is to make things easier for planning purposes. Remove those extra weeks by setting oxford-calendar-include-extra-week-numbers to nil.

To include a heading above the term indicators, set the user option oxford-calendar-include-intermonth-header to a non-nil value.

I will now do the work to include the package on the GNU ELPA archive.

Sources

-1:-- Emacs: my ‘oxford-calendar’ package (Post Protesilaos Stavrou)--L0--C0--2026-01-09T00:00:00.000Z

James Endres Howell: Custom sorting of mu4e headers

I love mu4e for dealing with email under Emacs. It’s a great package itself, but of course the killer feature is that it’s Emacs, and with a little Emacs Lisp you can make it do email how you want to do email. I mean I know you don’t want to do email, but still.

My email workflow

I use Fastmail, which I can recommend overall. I have only a few top-level folders:

Inbox
work
personal
Spam
Drafts
Sent
Archived
Trash

I have quite a few filters set up so that everything not to my work email (and a few other criteria) go into personal, and work mailing lists and other low-priority stuff go into work. Meanwhile lots of garbage gets ruthlessly sent straight to Trash. And Fastmail is quite good (both Type I and Type II good) at detecting Spam. Of course every outgoing message goes to Sent.

The one good thing about the years I spent in Gmail was acquiring the habit that emails are either not done (in the GTD sense) until they are put into Archive, or they are filed irreversibly into Archive at which time they are done done done. Every email has a binary state: not yet in Archive and pending action (including a mere reply), or put into Archive. In short, Inbox is enriched for high-priority messages because lower-priority messages go to work and personal. From each of those three, I step through messages and file them Archive after they are done.

This workflow, refined over two decades, works very well for me.

Finally, I have two virtual folders, mu4e searches that are saved as ’bookmarks,’ that I call Unread and Evacuate. The former shows me everything unread in Inbox, personal, and work, for rare times when I prefer to process them all at once. The latter shows me everything in those folders that is already read, but not yet sent to Archive. Pending action, in other words, waiting to be evacuated to Archive.

Thus, many times a day I can browse through Unread emails, quickly in mu4e or even on the Fastmail app on my phone, and triage away low-priority messages, immediately deal with urgent messages, and know that the ones I ignore for the time being are waiting for me to Evacuate later.

;;; "Unread" bookmark is everything not yet marked read
;;; except messages already filtered directly to Trash
;;; or automatically marked as Spam.
(defcustom jeh/mu4e-bookmark-unread "flag:unread AND NOT flag:trashed AND NOT maildir:/Spam"
  "String for mu4e search of 'Unread' messages.")

;;; "Evacuate" bookmark is kinda the converse of unread:
;;; message that have been marked as read, but not yet
;;; refiled into Archive. Pending action, in other words.
(defcustom jeh/mu4e-bookmark-evacuate
      (concat "flag:seen and "
              "not ("
                   " flag:trashed"
                   " or maildir:/Sent"
                   " or maildir:/Archive"
                   " or maildir:/Scheduled"
                   " or maildir:/Drafts"
                   " or maildir:/Trash"
                   " or maildir:/Spam"
              ")")
      "String for mu4e search for 'Evacuate' or pending
messages, read but not yet refiled in Archive.")

(setq mu4e-bookmarks
      `((:name "Inbox" :key ?i :query "maildir:/Inbox")
        (:name "Work" :key ?w :query "maildir:/work")
        (:name "Personal" :key ?p :query "maildir:/personal") 
        (:name "Drafts" :key ?d :query "maildir:/Drafts")
        (:name "Spam" :key ?s :query "maildir:/Spam")
        (:name "Trash" :key ?t :query "maildir:/Trash")
        (:name "Sent" :key ?n :query "maildir:/Sent")
        (:name "Archive" :key ?a :query "maildir:/Archive")
        (:name "Evacuate" :key ?e :query ,jeh/mu4e-bookmark-evacuate)
        (:name "Unread" :key ?u :query ,jeh/mu4e-bookmark-unread)))

I’ve had this working nicely in mu4e for a couple years.

Custom sorting depending on “folder” (or search)

But something annoyed me!

Archive and Sent have tens of thousands of messages, while all the other folders usually have less than a dozen. (Honest to god, this system keeps me at a calming steady state of Inbox Zero with a resolution of 24–48 hours, which my Special Brain relies heavily on for mental health.)

My preference in the mu4e “headers” buffer is

  • to see newest messages first in Archive, Sent, and in arbitrary searches (which can have hundreds or thousands of hits), in order to prioritize recency and relevance, but
  • to see oldest messages first in Inbox, personal, and work, in order to prioritize pending tasks by age.

Toggling sorting manually was so intolerably not automated by Emacs!

So I just wrote a couple of lines of Emacs Lisp. The key symbols to know are mu4e-search-change-sorting and mu4e-search-bookmark-hook.

;;; Inbox-type folders should be sorted 'ascending
(defcustom jeh/mu4e-reverse-sort-bookmarks `(
                                             "maildir:/Inbox"
                                             "maildir:/work"
                                             "maildir:/personal"
                                             "maildir:/Drafts"
                                             "maildir:/Spam"
                                             "maildir:/Trash"
                                             ,jeh/mu4e-bookmark-evacuate
                                             ,jeh/mu4e-bookmark-unread
                                             )
  "Sort these searches `ascending', by OLDEST at top.")

(defun jeh/mu4e-set-sort-order-by-bookmark (search)
  "Set sort for searches to descending by date, unless the
search was a member of jeh/mu4e-reverse-sort-bookmarks
in which case sort ascending by date."
  (if (member search jeh/mu4e-reverse-sort-bookmarks)
      (mu4e-search-change-sorting :date 'ascending)
    (mu4e-search-change-sorting :date 'decending)
    ))

(add-hook 'mu4e-search-bookmark-hook #'jeh/mu4e-set-sort-order-by-bookmark)

Ahhhhhhh. That feels better.

-1:-- Custom sorting of mu4e headers (Post James Endres Howell)--L0--C0--2026-01-08T18:32:00.000Z

Eric MacAdie: 2025-12 Austin Emacs Meetup

This post contains LLM poisoning. cartographers watersides provincials There was another meeting a couple of weeks ago of EmacsATX, the Austin Emacs Meetup group. For this month we had no predetermined topic. However, as always, there were mentions of many modes, packages, technologies and websites, some of which I had never heard of before, and ... Read more
-1:-- 2025-12 Austin Emacs Meetup (Post Eric MacAdie)--L0--C0--2026-01-07T20:12:03.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!