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

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

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!