Sacha Chua: Added Javascript time zone translation to the EmacsConf schedule images on the wiki

Nudged by Ihor Radchenko's suggestion, I added some JavaScript to translate the graphical SVG schedule on the EmacsConf wiki to your local time zone. I'll post the schedule on the wiki next week, but in the meantime, you can take a look at the draft schedule in the organizers' notebook.

Let me test it by setting my local time zone to UTC with sudo timedatectl set-timezone Etc/UTC…

2026-10-15-19-05-55.png
Figure 1: Screenshot of the draft schedule in the wiki, showing times dynamically translated to UTC

Here's the horizontal view in our backstage index, showing the status of talks so far.

2026-10-15-19-03-21.png
Figure 2: Screenshot of backstage view, showing times dynamically translated to UTC

I still need to make sure that my time zone is set to America/Toronto when I'm generating the SVGs, but at least now people can see the times translated to their local time zone.

The Javascript code is in https://emacsconf.org/script.js as addLocalTimeToSVGs. The Emacs Lisp code to generate these SVGs is in emacsconf-schedule.el. emacsconf-schedule-svg is the starting point.

View Org source for this post

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

-1:-- Added Javascript time zone translation to the EmacsConf schedule images on the wiki (Post Sacha Chua)--L0--C0--2026-10-15T18:48:04.000Z

Lars Ingebrigtsen: UX IS US

A month ago, I wondered idly whether translation gadgets (for quick word lookup) exist, and while everybody said “just use your phone, dumbo”, that’s just too sensible for me. And way too inefficient.

So while I pondered what to do about it, I bought some keyboards to see whether that could be the solution.

Small keyboards (and the one above is as small as they come) is such a cursed category. Either the keys are just too horrible or the range is awful, or a combination of the two. So I wasn’t expecting the above to work (a DarkWalker Wireless Bluetooth Keyboard, M4 Mini Keyboard), but… the keys don’t seem awful, really?

Let’s see how it works, though — I’ve paired it with my laptop, and I’m going to write the word “cheville”:

Yeah. It doubles up key presses like crazy. And this is paired with my laptop, so the distance between the keyboard and the Bluetooth receiver is just a few centimetres.

Trying several times, I have yet to make it through “cheville” correctly even once — in addition to doubling keystrokes, it also loses some here and there.

It also seems to go completely AWOL if you let it sleep after pairing? Which may be a Linux problem for all I know. The range isn’t awful — I get five metres easily.

But while it’s a nice form factor, it’s completely unusable. So onto the next:

This is a Phistone Mini Keyboard with Triple Mode, so it’s got a 2.4GHz USB dongle in addition to the Bluetooth. Which, in my experience, usually works better on these types of things.

But man, that’s one ugly keyboard… and it works! I’m not getting any missed keystrokes or any doubled ones. So let’s start implementing something!

After typing away a bit (the results are on Microsoft Github), I’ve now got something that seems to work.

Let’s see… here’s my normal position in my reading chair, and I have the li’l ugly keyboard on the table next to me, and there’s a screen over there that normally displays the cover artwork of the album that’s playing on the stereo… So let’s see how efficient this is:

Hey! This actually kinda sorta has possibilities…

The Emacs over there is querying the Microsoft Azure Translation API behind the scenes — I tried a couple of other services first.

DeepL just returns one word, for instance, which is pretty useless when looking up words (because most have several meanings):

(deepl "cheville")
=> (:translations [(:text "dowel" :detected_source_language "FR")])

I was looking for the more normal meaning of “ankle” here…

Anyway, Azure API is free, and returns a list of resulting words and word classes, so that’s pretty ace:

(aztrans "cheville")
=> (:source "cheville" :translations ((:target "ankle" :type "NOUN")
				      (:target "peg" :type "OTHER")
				      (:target "pin" :type "NOUN")))

Thanks, Microsoft! Not everything you make is a crime against humanity!

(I mean… that I know of…)

The Emacs package works by grabbing the evdev device for the li’l keyboard, so that X doesn’t see any of the events. And then it just pops to a new window configuration and displays the input, and then finally queries the Azure API before popping back to where you were…

So this works, but that keyboard really is too big to be on my table here permanently. Does anybody know of a keyboard that’s more like that first keyboard, size wise, while working as well as this larger keyboard? Or perhaps not that small — something halfway between these two keyboards would be ideal. And not more keys than are needed — the small number of keys on that M4 keyboard is nice.

-1:-- UX IS US (Post Lars Ingebrigtsen)--L0--C0--2026-10-15T16:14:13.000Z

Emacs-fu: mu 1.1.14

‎

mu4e 1.14.4

I haven't written here in a while (those 13 years just flew by!) so it's about time to add something new, and I still beat The Winds Of Winter! While I haven't blogged much, I'm still using Emacs every day, and am still working on mu/mu4e, the Emacs e-mail client.

There have been quite a few changes in mu and mu4e in those years (think: Theseus' Ship). Lately, there's a new version every few months, leading up all the way to release 1.14.4 today.

I'll go through a number of useful features you may not yet know about. Some are new, some not so new. You can track all new features / changes in NEWS.org. Below changes are available in 1.14 or newer, unless mentioned otherwise.

Inbox zero: Adding tasks and refiling

Message refiling (moving message to some message-specific folder) is one of the ancient, defining features of mu4e, inspired by the mutt mail-reader; I use it all the time, together with the Org-mode support.

Following the "Inbox Zero" philosophy, I go through my inbox a few times per day:

  1. delete: For many/most messages: remove (e.g., most mailing-list messages).
  2. todo: If some further action is required, add a TODO item to my Org agenda (see below).
  3. refile: Move the message to the appropriate archive folder

After that, my inbox is nice & empty, "zero" again; David Allen would be proud! Let's look at (2) and (3) in a bit more detail.

Adding todo-items from mail

For messages that require some further action, I add a todo-item to the Org-mode agenda. For this, I use a number of capture templates.

You can start with something basic; add to org-capture-templates:

("m" "Handle mail" entry (file+headline "~/Org/todo.org" "Todos")
  "* TODO %a from %:fromnameoraddress\n\t%u\n ")

The TODO-items link to the message they originate from, based on its message-id, so you can jump (C-c C-o) to that message without a sweat, regardless of where you moved it.

Refiling messages

As I walk through my inbox, I move messages I want to keep to some appropriate archive folder, and you can instruct mu4e to pick a good one for a given message.

You can make this arbitrarily complicated, but perhaps as a starting point on your refiling journey, you could start by configuring mu4e-refile-folder something like:

(setq mu4e-refile-folder
   (lambda (msg)
     (cond
      ((mu4e-message-sent-by-me msg) "/sent") ;; sent by me -> sent
      ;; all mu-related messages go to mu/
      ((or (mu4e-message-contact-field-matches msg :to "mu-discuss@googlegroups.com")
           (mu4e-message-contact-field-matches msg :cc "mu-discuss@googlegroups.com")
           (string-match "\\[mu\\]" (or (mu4e-message-field msg :subject) "")))
       "/mu")
      ;; example messages go to the examples folder
      ((seq-some
        (lambda (addr) (mu4e-message-contact-field-matches msg :from addr))
        '("@example1\\.com$" "@example2\\.com$"))
       "/examples")
      ;; if I'm an explict recipient, move to the "personal/" folder
      ((mu4e-message-personal-p msg) "/personal")
      ;; everything else goes to /bulk
      (t
       "/bulk"))))

mu4e 1.14.4 adds some handy aliases to make this less unwieldy, and you can write:

(setq mu4e-refile-folder
   (lambda (msg)
     (cond
      ((mu4e-message-sent-by-me msg) "/sent") ;; sent by me -> sent
      ;; all mu-related messages go to mu
      ((or (mu4e-contact-matches-p msg :recip "mu-discuss@googlegroups.com")
           (mu4e-subject-matches-p msg "\\[mu\\]"))
       "/mu")
      ;; example messages go to the examples folder
      ((mu4e-contact-matches-p msg :from '("@example1\\.com$" "@example2\\.com$"))
       "/examples")
      ;; if I'm an explict recipient, move to the "personal/" folder
      ((mu4e-message-personal-p msg) "/personal")
      ;; everything else goes to /bulk
      (t
       "/bulk"))))

As with anything Emacs / mu4e / org-mode: start simple and complicate things as needed… I suppose it's fine to procrastinate a little bit on tweaking productivity tools, but of course it's time that could have been spent on watching TikTok videos.

MIME Type icons

A small cosmetic feature; if you have some "icon package" such as Nerd Icons or All the Icons, you can tell mu4e to use them to decorate MIME-parts / attachments, by setting mu4e-file-name-to-icon-function to nerd-icons-icon-for-file or all-the-icons-icon-for-file. For nerd-icons I found some extra tweaking may be needed, so if you get wrongly sized icons, you can try something like:

(setq mu4e-file-name-to-icon-function
  (lambda (name)
    (nerd-icons-icon-for-file name :height 1.0)))

Completion

mu since 1.14.3 includes command-line auto-completion support for Bash, Zsh and Fish (experimental, 1.14.4). Useful, since there are so many options/flags to choose from.

Human Language detection

When built with CLD2 support, mu attempts to detect the human language a message was written in, and you can search for it using the lang: field.

E.g. to find all messages in Finnish:

$ mu find lang:fi

Attachments in replies

Normally, a message reply does not include any attachments; however, for certain kinds of attachments such as patches/diffs, it is useful to include an inline version in the reply, so you can comment on it; and that is what mu4e does now; configurable with mu4e-compose-reply-include-mime-types.

HTML messages

In the last few releases, mu4e has seen many improvements for dealing with HTML messages.

  • Switch-to-html (mu4e-view-toggle-html, h) now works for all messages, which is useful for showing them in the browser, printing etc. (1.14.3)
  • In plain-text mode, we no longer show the [1] [2] link numbers until you need them (after mu4e-view-go-to-url (g) or mu4e-view-save-url (k)) (1.14.3)
  • Keyboard-handling for the link functionality is now also integrated with HTML-mode (using Emacs' completion interface) (1.14.4)

Back from the dead: xwidget for HTML messages

You can use Emacs' Xwidget support to show emails in a webkit widget (GTK3/pgtk). Unfortunately, xwidget support in Emacs had bitrotted (i.e., only worked with long-obsolete webkit versions) in the last few releases.

Fortunately, it wasn't too hard to fix this, and my efforts have landed in Emacs 31.1: xwidget is back!

With 1.14.4, I have integrated it a bit further in mu4e as well (still experimental!); xwidget is still a bit un-Emacsy, but mu4e tries to terraform it.

(setq mu4e-view-html-renderer 'xwidget) ;; default is the built-in 'shr

shr is fine for most messages, but some (looking at you, airlines!) can be hard to decipher, and xwidget support makes those readable.

[ One sore point is that your (GTK3/pgtk) Emacs >= 31.1 probably does not have xwidget support yet, unless you build it yourself (--with-xwidgets). Understandably, distributions don't build Emacs with xwidgets since they couldn't for a few years; let's hope support returns now. ]

Desktop notifications

For a while, mu4e has supported desktop notifications for new mail; this uses Emacs' notification support, and is as simple as setting:

(setq mu4e-notification-support t) ;; set before starting mu4e

This gives you some pop-up when there's new mail; see the mu4e manual for details.

More recently, mu4e gained DBus service support, which allows for richer interactions if you are on a platform with DBus. Assuming your Emacs has DBUS support, you can enable it in your configuration with:

(mu4e-dbus-mode)

It is all still a bit in the IKEA-stage, but there's a script mu4e-dbus-client in the mu sources, which allows you to add read/unread counts to Waybar, with something like:

"custom/mu4e": {
 "exec": "~/src/mu/contrib/mu4e-dbus-client --format waybar",
 "return-type": "json",
 "interval": 120,
 "format": "{text}{icon}",
 "format-icons": {
   "empty": "📭",
   "unread": "📬",
   "new": "📧"
  },
 "tooltip" : true
}

Add custom/mu4e to your modules-right|left|center as desired, to get something like:

The tooltip has read/unread counts for your bookmarked queries; nothing too spectacular, but it shows it's possible, so perhaps it can inspire you, dear reader, to add support for your favorite desktop bar.

Author: Dirk-Jan C. Binnema

Created: 2026-10-15 Thu 18:51

Validate

-1:-- mu 1.1.14 (Post Emacs-fu)--L0--C0--2026-10-15T15:54:38.318Z

Irreal: Saving Window Configurations

Over at techhub.social, lispwizard has a short post (or whatever techhub.social calls them) on saving window configurations to registers. He notes how easy it is to save multiple configurations and switch between them on the fly.

Saving window configurations and other data in registers is an extraordinarily powerful technique that all Emacs users should learn and master. There’s a problem, though, when you want to do this programmatically: which register should you use? How can you be sure that your choice won’t conflict with a register that the user has chosen manually for some other task? There are several situations in which I want to run an Emacs function in a single window—Elfeed is an example—and I do that by saving the current window configuration, deleting the other windows, running the required function, and then restoring the previous window configuration but how do I choose a “safe” register to save the window configuration in?

The answer is a trick I learned from Magnar Sveen years ago. Instead of using a letter to name the register, you use a symbol like :saved-windows. That works because when you name a register with a letter, Emacs uses a pointer to its symbol as the register name. Using a symbol name like :saved-windows works because the result is also a symbol pointer.

If you have the need to save something in a register programmatically, this is a good way of doing it. You don’t have to worry about conflicts because the user can not use symbol names for registers interactively.

-1:-- Saving Window Configurations (Post Irreal)--L0--C0--2026-10-15T15:02:25.000Z

Protesilaos: Emacs live continuing ‘denote’ maintenance and refinements (2026-10-15 16:30 Europe/Athens)

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

[ The stream will be recorded. You can watch it later. ]

In a couple of hours I will resume the work I am doing on my Emacs denote package. There are general refinements I wish to make, but also to continue iterating on the new denote-data-mode, which is an optional in-memory cache for Denote.

I will have the chat open in case someone has a question about what I am programming or ANYTHING else they might want to ask me.

-1:-- Emacs live continuing ‘denote’ maintenance and refinements (2026-10-15 16:30 Europe/Athens) (Post Protesilaos)--L0--C0--2026-10-15T00:00:00.000Z

Irreal: Wizard Tools

Yesterday I wrote about James Cherti’s Org-ibullets package. Today I’m writing about another package that he announced at essentially the same time: wizard.el. It’s a package of some carefully written Emacs functions dealing mostly with editing and navigation.

Cherti describes his functions as using the builtin Emacs functions rather than third party packages or writing complex logic de nuevo. It’s surprising how far you can get with just what Emacs provides out of the box.

You can look at Cherti’s post or the source code for a list of the functions but their broad categories are:

  • Highlighting functions
  • Buffer and State Management
  • Smart Text Manipulations
  • Search & Replace
  • Code Navigation

Cherti has more to say about the functions in those categories so be sure to take a look at his announcement. If one of your goals is to reduce the number of third part dependencies in your configuration, this package may interest you.

-1:-- Wizard Tools (Post Irreal)--L0--C0--2026-10-14T16:13:24.000Z

Andros Fenollosa: Org Social: the social network that survives its own infrastructure

Org Social is so simple it looks like it was born before the internet. A single plain text file, an editor and a host are enough to interact with other people.

To "create an account", you just create a plain text file in any editor. Your "database" is your own hierarchy of posts. Your "algorithm" is however you decide to sort other people's posts. There is no "character limit". Your "server" is any hosting or web server. Your "interface" is your favorite editor. And if you want to "delete your account", you just delete the file. And of course, the syntax is richer than Markdown and just as easy to learn: Org Mode. You share and edit wherever you want, and you use it as a microblog, a journal, a forum, a bulletin board or a blog. Your file, your rules.

Behind it there is infrastructure that enriches the interaction and connects it with other systems and formats (the Fediverse, RSS, clients...). Each piece adds one more level of convenience, but none of them is essential: they are 100% optional! And that is what I want to talk about.

I'd like to take it apart piece by piece, explaining what each one does and why it exists.

Layer 0: a plain text file

Everything starts and ends here. A file called social.org, in UTF-8, with LF line endings.

The header is your profile. They are Org Mode keywords:

#+TITLE: My journal
#+NICK: Alice
#+DESCRIPTION: I write about Emacs and coffee
#+AVATAR: https://alice.com/avatar.jpg
#+LINK: https://alice.com
#+FOLLOW: https://bob.com/social.org
#+FOLLOW: friend https://carol.net/social.org

Below it, in a * Posts section, go your posts. Each one is a heading with an RFC 3339 timestamp, which works as a unique identifier:

** 2025-05-01T12:00:00+0100
:PROPERTIES:
:LANG: en
:TAGS: emacs org-social
:END:

Today I discovered Org Social and I can't stop writing.

And you already have a social network.

Anyone can curl your file, read it with their own eyes or with a client or script, and follow you by adding your URL to their #+FOLLOW:.

The interaction itself lives inside the format. There are no magic buttons, there are properties:

  • Replying is a post with :REPLY_TO: <url>#<id> pointing to the original.
  • Reacting is a reply with just :MOOD: ❤ and an empty body.
  • Sharing is :INCLUDE: with a reference to someone else's post.
  • Voting in a poll is replying with :POLL_OPTION:.
  • Mentioning someone is a link [[org-social:https://bob.com/social.org][Bob]].

Since it is plain text, you can edit all of it by hand if you feel like it.

When you are done, you save the file and upload it to your server.

Every decentralized social network has a weak spot, whichever one comes to mind. I covered this in There is no such thing as full decentralization, but in short, you always have to decide which of these five things the network sacrifices:

  • Identity
  • Connectivity
  • Discovery
  • Cost
  • Moderation

Org Social sacrifices discovery. Nobody can find you unless you give them your URL, just like an email address. And that makes some interactions impossible to reconstruct. For example, what happens if someone you don't follow joins a thread you started? If you don't follow them, you never find out.

That's why we need upper layers that mitigate the problem, although you should only use them if you think you need them. It may be that everyone taking part follows each other and needs nothing else. Otherwise, you will need a Relay: an index.

Layer 1: the client

You can check each feed directly with curl or with your browser. However, just like with your list of RSS/Atom feeds, what we all end up doing for convenience is using a client. It reads every source, sorts them, filters them and merges them into a list that is easier to read and that you can also navigate. The same goes for Org Social.

There is a client for Emacs, another for iOS, another for Android (in alpha as I write this), command line implementations and a public web viewer that needs no account or client. Clients build a timeline by merging all the feeds you follow, and they let you reply, react, share and vote in polls. But every change happens in your own file, not on a central server, and reading is volatile, since it is built on the fly.

Screenshot of org-social.el in Emacs: the Org Social timeline with navigation buttons, posts with hashtags, reactions, author and an embedded image

The client also acts as a translator between Org Social and the interface you choose. And it is a good way in for people who are already used to X/Twitter or Mastodon, or who don't want to bother learning the syntax.

Layer 2: the Relay

The Relay is a server that crawls the network of social.org files and builds an index: users, mentions, replies, groups and threads. It does this by reading the #+FOLLOW: lines of every feed it knows and downloading the files it finds. You can also register your feed manually so it reads it.

It doesn't store content, only addresses. It notes who mentions whom, what replies to what and what belongs to each thread.

Now you can write a client or a script that asks the Relay for the information that fills in the parts where you were blind before:

# Who mentioned me?
curl "https://relay.org-social.org/notifications/?feed=..."

# Give me the full thread of this post
curl "https://relay.org-social.org/replies/?post=..."

# Find everything about Emacs
curl "https://relay.org-social.org/search/?q=emacs"

And that's not all: anyone can run their own Relay, locally or on a server. Their instance will connect to other public Relays and they will exchange information, so the whole network knows which feeds exist and how they relate.

It also offers other features, such as real-time notifications, RSS/Atom feeds, post search and bridges to ActivityPub/Mastodon. Yes, you can follow Mastodon accounts from Org Social, or keep up with Org Social from your favorite RSS/Atom reader.

Layer 3: Org Social Host

Needing your own hosting just to start taking part in the network is a tough requirement. That's why there is a free and open hosting service, integrated with the clients, that puts your social.org online with a single click.

You pick a nick and get two addresses:

  • public-url: the one you share with everyone, for example https://host.org-social.org/alice/social.org.
  • vfile: a secret, signed URL that works as a password. Whoever has it can overwrite your file, so keep it safe.

If you use a client, all of this is automatic and you never even see the secret URL, since it is stored in your configuration. It all happens behind the scenes.

If you are curious, signing up is a single request:

curl -X POST https://host.org-social.org/signup \
  -H "Content-Type: application/json" \
  -d '{"nick": "alice"}'

And overwriting your file, another one:

curl -X POST https://host.org-social.org/upload \
  -F "vfile=YOUR_VFILE" \
  -F "file=@social.org"

It doesn't lock you in either. You can ask it for a permanent redirect (HTTP 301) to your new URL and your followers won't even notice. Their clients follow the redirect on their own, although it's a good idea to let them know so they update their #+FOLLOW:. It is a way in, not a requirement.

You can also run it on your own server, with public sign-up disabled and your own domain.

And it has an advantage that seasoned app developers will have guessed already: it makes syncing between devices easy. You can, for example, write in the mobile client and carry on in the Emacs one with simple HTTP requests. No old protocols, no third-party APIs.

Layer 4: Mirror or CDN

Do you use an RSS/Atom reader? Then you already know that the more feeds you follow, the slower it gets.

It doesn't matter how fast and parallel the downloads are. There will always be dropped connections, timeouts, errors and redirects. And if a feed is very large, the client has to parse all of it to keep what it cares about.

In my case, I follow 250 feeds. Download and parse them every time I scroll? Not viable. Cache them? Then the problem becomes when to invalidate the cache.

This is where the Mirror comes in. It is a server that keeps an up-to-date copy of every feed and saves you the parsing, sorting, thread building and mention consolidation. It exposes a simple REST API that returns JSON ready to use in a client.

You can think of it as a client of clients, or a general-purpose client without an interface. Internally, it works non-stop to keep the most recent copy and to prepare the processed information so it is easy to consume.

For example, if I want to build the timeline with Bob's and Carol's posts from the last 14 days:

curl --get "https://mirror.org-social.org/api/posts/" \
  --data-urlencode "following=https://bob.com/social.org" \
  --data-urlencode "following=https://carol.net/social.org" \
  --data-urlencode "sinceDays=14"

It doesn't replace the Relay. In fact, it needs it to know which feeds exist and which ones have changed. And most importantly: it is read-only. It never edits or deletes anything at the source. To publish, you still write to your own file. Its only way in is a webhook that a client can call to say a feed has changed and the Mirror should update it:

curl -X POST "https://mirror.org-social.org/api/webhook/refresh/" \
  -H "Content-Type: application/json" \
  -d '{"feed": "https://alice.com/social.org"}'

It is optional. The client calls it right after publishing or uploading a new version of the file to your hosting, so the Mirror updates its copy straight away instead of waiting for the next refresh cycle.

Layer 5: Push

The Relay already sends real-time notifications with Server-Sent Events, which clients can listen to in order to tell you about mentions, replies and reactions. But phones don't let an app keep connections open in the background, so there you need push notifications. It is the most specific layer of all, since each platform, iOS and Android, has its own notification system with its own certificates and tokens.

If this layer goes down, all you lose is the convenience of getting alerts on your phone while the app is closed.

Layer 6: Live Preview

An Org Social post has an address in the format https://<host>/social.org#<timestamp>. For example, my first post: https://host.org-social.org/andros/social.org#2025-08-14T09:11:00+0200. It works fine between Org Social clients, but what happens if you want to share it on another social network? Nothing pretty: the whole social.org file gets shared.

Live Preview turns a post into a web page. You give it the post URL, it downloads the original social.org (straight from the source, not through the Relay), finds the post and generates an HTML card with its Open Graph metadata: nick, avatar, text and date. Ready to share on any social network.

For example, the URL above becomes:

https://preview.org-social.org/?post=https%3A%2F%2Fhost.org-social.org%2Fandros%2Fsocial.org%232025-08-14T09%3A11%3A00%2B0200

Live Preview card of my first Org Social post: avatar, the nick andros, the text "Hello Org Social." and the date 2025-08-14T09:11:00+0200

And if I share it on social media, it looks like this:

WhatsApp screenshot after pasting the Live Preview link: the card shows my avatar, the nick andros, the text "Hello Org Social." and the domain preview.org-social.org

Clients also use this link format in their share option.

The same need gave birth to another very interesting feature: generating an HTML blog from your social.org, if it is hosted on host.org-social.org.

For example, my Org Social web version would be preview.org-social.org/blog/andros.

Screenshot of the Live Preview blog at preview.org-social.org/blog/andros: a header with my avatar, the title Andros' journal and my description, the pinned post "Hello Org Social." and my latest posts below with date, links and tags

You write in Org Mode, upload the file to the hosting (with a simple curl if you like) and your HTML blog is generated automatically, ready to share. It isn't perfect, since you don't control the design, but it saves you from running a static site generator.

Summary

In the end, they are not layers, but pieces orbiting around your social.org.

flowchart TB
    File["social.org"]
    Client["Client"]
    Relay["Relay"]
    Host["Host"]
    Mirror["Mirror"]
    Push["Push"]
    Preview["Live Preview"]

    Client -- "reads and writes" --> File
    Client -- "uploads and syncs" --> Host
    Host -- "hosts" --> File
    Host -- "registers feeds" --> Relay
    Relay -- "indexes" --> File
    Client -- "mentions, threads and search" --> Relay
    Mirror -- "which feeds exist and which change" --> Relay
    Mirror -- "copies" --> File
    Client -- "JSON timeline and webhook" --> Mirror
    Relay -- "real-time notifications" --> Push
    Push -- "phone alerts" --> Client
    Preview -- "reads" --> File
    Client -- "share link" --> Preview
Piece What it gives you If you remove it
social.org Your identity and your posts There is no network
Client Reading and writing comfortably You read with curl and edit by hand
Relay Discovery, mentions, threads and bridges You only see who you follow
Host Hosting without a server and syncing between devices You use your own hosting
Mirror Ready-to-use JSON, no Org parsing The client downloads and parses every file
Push Alerts on your phone You find out when you open the app
Live Preview Sharing posts outside the network You share the raw file URL

Whatever you use, the only essential piece is your social.org.

If every Org Social server shut down tomorrow, what would you lose? Convenience. And what would you keep? Your file, intact, on your disk. You upload it anywhere else and keep posting.

Few social networks can say the same.


Help me keep writing Every coffee gives me a push toward the next article. Sure, it's on me!

Send an email to comment+article-6c165f77@andros.dev to leave a comment. The subject will be ignored.

-1:-- Org Social: the social network that survives its own infrastructure (Post Andros Fenollosa)--L0--C0--2026-10-14T15:12:18.000Z

Magnus: Emacs salmagundi, 2026-10-11

Combining lists of compilation commands

To my delight I found that emacs-multi-compile will combine the of all matches compilation commands!

I added this to my setup

(add-to-list 'multi-compile-alist
               '((mes/is-in-git-project) . (("git clean -n" "git clean -ndx" (project-root (project-current)))
                                            ("git clean -f" "git clean -fdx" (project-root (project-current))))))

and those commands are now shown together with the ones I defined for Haskell project (see my previous post) when both match.

Recognising a git project

To add the git commands to multi-compile-list I needed a function to determine if the current buffer is in a git working tree. I failed to find something fitting so I ended up adding this little function.

(defun mes/is-in-git-project ()
  "Check if we're currently in a git project."
  (when-let* ((proj-dir (project-root (project-current))))
    (vc-git-root proj-dir)))

Taming compilation mode buffers

I'm using display-buffer-alist to control how Emacs places buffers. For compilation mode I had the following actions.

(display-buffer-reuse-mode-window display-buffer-in-side-window)
(mode compilation-mode)
(window-height . 0.25)
(side . bottom)
(post-command-select-window . t)

That last command made switching buffers with consult-buffer a bit irritating as previewing would interrupt the buffer switching by selecting the compilation window. However, I really want the compilation window selected after a compilation. Or rather, as I realised after reflecting a bit, I want the compilation window selected if there are errors.

Compilation mode has a way of running functions when a compilation finishes so I defined two functions and added them to compilation-finish-functions.

(defun mes/close-successful-compilation (buf msg)
  "Close compilation buffer after success."
  (when (and (buffer-live-p buf)
             (with-current-buffer buf (eq major-mode 'compilation-mode))
             (string-match "finished" msg))
    (run-with-timer 3 nil
                    (lambda ()
                      (kill-buffer buf)))))

(defun mes/select-failed-compilation (buf msg)
  "Select compilation buffer after failed compilation."
  (when (and (buffer-live-p buf)
             (with-current-buffer buf (eq major-mode 'compilation-mode))
             (string-match "abnormally" msg))
    (select-window (get-buffer-window buf))))

  (add-to-list 'compilation-finish-functions #'mes/close-successful-compilation)
  (add-to-list 'compilation-finish-functions #'mes/select-failed-compilation)

With that a successful compilation is closed automatically after 3s and on a failed compilation the compilation buffer is selection.

-1:-- Emacs salmagundi, 2026-10-11 (Post Magnus)--L0--C0--2026-10-13T19:46:00.000Z

Irreal: Org-ibullets

The ever-officious minions have staged what they’re calling an intervention. They told me that I’ve milked this illness shtick long enough and that it was time to get back to work. They were unmoved by my protestations that I was not yet 100 percent and merely pointed at my computer. They obviously don’t understand the meaning of “minion”. Anyway, I’m trying to get things back to normal.

James Cherti has announced a simple package that trades Org mode’s heading stars for UTF-8 bullets. If you’re like me, your first thought is, “Doesn’t that already exist?” Sort of. What already exists is the no longer maintained org-bullets. Cherti’s package is called org-ibullets (note the i).

Cherti explains the differences from org-bullets in his announcement. The TL;DR is that he’s optimized org-ibullets by eliminating redundant computations, eliminated spurious syntax warnings that org-bullets could produce, and made some improvements at the source level.

As I said, the package is simple. It can be used right out of the box but you can customize the list of bullets and their face if you desire. This seems like a very nice package if you’d like to make your Org buffers look a little nicer.

-1:-- Org-ibullets (Post Irreal)--L0--C0--2026-10-13T15:45:11.000Z

Alvaro Ramirez: agent-shell 0.87 updates

It's October. New month, new agent-shell update. If you missed the last post, have a look at the 0.78 update. While the latest changes are focused primarily on bugfixes and polish, we have a handful of new features too.

As usual, this post showcases highlights, but please check out the full list of changes if you're after the nitty-gritty.

What's agent-shell?

agent-shell brings your favorite AI agents to Emacs, keeping you at home: familiar buffers and bindings, no terminal emulation. All powered by ACP, the Agent Client Protocol.

Happy birthday agent-shell

I introduced agent-shell just over a year ago, with support for two agents: Claude and Gemini. Today, it speaks to 23.

Development hasn't let up either. Improvements and new features land every month.

Thank you to all users, contributors, and sponsors.

New supported agents

A couple more join the family, bringing it to 23 agents. As usual, they're all reachable via M-x agent-shell, or go straight to the new ones:

Animated prompts

With the ability to prompt at any time (see my last post), you can queue prompts or steer agents straight from the persistent shell prompt. Animating the prompt ties it all together: it's now obvious at a glance that the agent is busy, and that anything else you submit will queue.

Check out agent-shell-prompt-busy-frames for relevant customizations (more on that shortly).

Thanks to @cark for the proposal.

Busy indicators get further customizations

The header's busy indicator has been animated for some time now. Amongst the existing customizations, agent-shell-show-busy-indicator could turn it off altogether. Having said that, there's now a middle ground: use static text via agent-shell-busy-indicator-frames.

Just set it to a string like so:

(setopt agent-shell-busy-indicator-frames "(busy)")

If you'd like to use smarter logic, you can now set it to a function as follows:

(setopt agent-shell-busy-indicator-frames
        (lambda ()
          (if (agent-shell-session-id)
              "(busy)"
            "(connecting)")))

agent-shell-prompt-busy-frames takes all the same forms, so the prompt's own animation can also be a list of frames, a static string, or a function.

Queue merging

With the new agent-shell-prompt-queue-merge setting (enabled by default), pending prompts are joined (separated by blank lines) and submitted as a single turn. Queued prompts are typically follow-ups to the same instruction, so the agent now sees them together rather than acting on the first before reading the rest.

If you prefer the previous behaviour, disable it with:

(setopt agent-shell-prompt-queue-merge nil)

Thank you @Gleek for the proposal.

While on the subject of queueing, cancelling now offers to continue with the queue rather than quietly dropping it, and killing a shell now warns you about the pending work you'd lose.

More idiomatic completion

@ and / completion used to bundle two things: how to complete, and when to pop up. Turning the popping off lost completion altogether, leaving corfu and company users out.

They're now decoupled. The completion functions are always registered, so completion-at-point, corfu and company work either way. Two options decide whether completion pops as you type: agent-shell-file-completion-enabled for @ and agent-shell-command-completion-enabled for /. For example, listing files can be slow in a large project, so to only complete @ on demand while / keeps popping:

(setopt agent-shell-file-completion-enabled nil)

Trailing spaces (and @ mention quoting) are now only inserted on a finished completion, fixing the stray spaces corfu and company users were seeing.

Thanks to @bcc32 for proposing the split and separate @ and / options in #537, and to @jcubic for filing it and flagging the stray spaces.

Moving by blocks

C-M-a and C-M-e (beginning-of-defun and end-of-defun) now move between blocks. Thank you @OSadovy for #876.

Customizable chat labels

Chat mode draws "Me" and "Agent" labels (or rather "Claude", "Codex" etc.). If the current style isn't your jam, agent-shell-chat-label-function now lets you draw them yourself. It's handed an alist with :text and :role (either user or agent), and must return a single-line propertized string.

The display property is honored, so labels can also be rendered as images. Thanks to @nhojb for the idea.

Parseable transcripts

Transcripts now open with YAML frontmatter, including a start timestamp as follows:

---
agent: "Claude"
started: "2026-10-11T09:30:12+01:00"
working_directory: "~/stuff/active/code/agent-shell"
session_id: "0f3c9a1e-5b7d-4e2a-9c81-6d4f2b8e7a35"
model: "claude-opus-4-5"
---

Thank you @vermiculus for #854.

While on topic, transcripts also got a little sturdier, thanks to @mrcnski's proposal.

M-x yank-media support

M-x yank-media now pastes images straight from the system clipboard (#853), no external utilities needed, the same way Org mode handles it. Pasted image previews also survive being queued or steered, thanks to @Gleek for #852.

Codex config options

Codex gained agent-shell-openai-default-config-options for setting config options at session start, reasoning effort included. For example:

(setopt agent-shell-openai-default-config-options
        '(("reasoning_effort" . "high")))

Thank you @ianthetechie for #855.

Tool call output

Tool call output can be fairly long. A single chunky line was enough to bog a buffer down. Long tool output lines are now limited (#844 by @Gleek), and fallback titles are truncated so they no longer flash chunkily as they stream.

File summaries also count distinct files now, so reading the same file three times counts as one file rather than three (#863, also by @Gleek).

A bunch of fixes

While we have a handful of new features, the bulk of this release went into fixes.

Rendering:

  • Emphasis no longer swallows asterisks inside inline code (#870).
  • Bold around inline code now renders, as does a trailing quote or header line.
  • Streamed tables keep their alignment and markup (#868).
  • M-x agent-shell-copy-as-markdown no longer mangles bullets carrying bold text (#867).
  • Expanding or collapsing an item inside a group now works mid-stream.
  • Chat mode no longer hides notices rendered above the live prompt.
  • The graphical header no longer gets cut off when showing cost (#849).
  • Terminal hyperlinks (osc-8) are rendered (#850 by @gnufied).

Performance and responsiveness:

  • Severe cursor lag from large collapsed tool outputs is gone (#839).
  • Bare path:line references are now found in linear time (#848 by @catern), and failed searches leave point where it was.
  • Heartbeat ticks skip redrawing when the busy frames haven't changed.
  • In acp.el, draining the agent's output is now bounded in how long it holds the main thread (#32 by @moyotar), so a chatty agent no longer makes Emacs feel laggy.
  • In shell-maker, launching a shell with visual-wrap-prefix-mode enabled no longer hangs Emacs (#842).

Elsewhere:

  • Forking resumes the session, and keeps the fork rather than falling back to another session if that resume fails (#871).
  • Grouped config options are handled (#851), and a missing config change event is now emitted (#864).
  • display-buffer-overriding-action is preserved when displaying asynchronously (#846 by @bcc32), and docstring command binding specifiers are fixed (#845, also by @bcc32).
  • Shell command output now runs in the shell's directory via its file handler (#879 by @junyi-hou), which matters for TRAMP users.
  • The README gained guidance for agents hanging at "Loading…" over TRAMP (#872), which turned out to be about tramp-remote-path and direct async processes rather than a bug.

News on the welcome screen

New shells now show the last few release posts on the welcome screen, so they are a little more discoverable.

Public functions

A few additions for package authors building on agent-shell.

Knowing when a shell is ready

agent-shell-initialized-p returns non-nil once the handshake, authentication, session and any default options are all done. Until then, subscribe to the new init-finished event via agent-shell-subscribe-to (#860).

Reading and setting config options

Agent config options are now reachable from elisp rather than the UI alone:

(agent-shell-config-options)
(agent-shell-config-option :category "thought_level")
(agent-shell-config-option-value :id "effort")
(agent-shell-set-config-option-value :id "effort" :value "high"
                                     :on-success (lambda (_) (message "Set")))

Other bits

  • agent-shell-last-activity-time returns when the shell last saw a submitted prompt or an incoming notification (#856), handy for idle tracking.
  • M-x agent-shell-save-traffic writes the session's ACP traffic to a .traffic file and reveals it in dired, simplifying the bug-filing process.

New third-party package

agent-shell-gc joins the lot, bringing buffer garbage collection (auto-kill) to agent-shell.

Housekeeping

If you peeked at the commit logs for the period, you'll see it's been a busy few weeks. Since the last post, 125 commits shipped, 33 issues have been closed and 15 pull requests merged. As of this writing, the backlog sits at 4 open issues and 6 open PRs (versus 16 and 6 last time around).

Zooming out a little, here's how the backlog has tracked:

As usual, this chart was generated using the /github-activity skill shared in my emacs-skills repo.

Four open issues is the lowest it's been since the project's first week, which I'm pleased about.

All of this requires daily attention.

Paying for tokens? Consider funding agent-shell.

Tokens keep your agents running. Sponsors keep agent-shell running.

agent-shell is built and maintained by me, an indie dev, while the tools it competes with have well-funded teams behind them. Vendor-neutral tooling matters more than ever.

Using agent-shell at work? Ask your employer to sponsor too. Companies can contribute at a scale individuals can't.

Pull requests

Thank you to all contributors for these improvements!

agent-shell

  • #797: Render ACP elicitation forms (@OSadovy)
  • #844: Limit long tool output lines (@Gleek)
  • #845: Fix docstring command binding specifiers (@bcc32)
  • #846: Preserve display-buffer-overriding-action when displaying asynchronously (@bcc32)
  • #847: Add Junie (@thanhvg)
  • #848: Find bare path:line references in linear time (@catern)
  • #850: Add support for rendering browse-url-data with osc-8 sequences (@gnufied)
  • #852: Preserve pasted image previews in queued and steered prompts (@Gleek)
  • #854: Emit parse-able YAML frontmatter for transcripts (@vermiculus)
  • #855: Add tunable for default Codex config options (@ianthetechie)
  • #862: Mention agent-shell-gc package in the README (@izeigerman)
  • #863: Count distinct files in read, edit, and delete summaries (@Gleek)
  • #876: Move by blocks with beginning-of-defun and end-of-defun (@OSadovy)
  • #878: Add OpenClaw agent support (@jtmoulia)
  • #879: Run command output in shell directory via file handler (@junyi-hou)

acp.el and shell-maker

agent-shell stands on acp.el to speak ACP and shell-maker for its shell foundations, so fixes landing there reach you just the same:

  • acp.el #32: Bound how long a single drain holds the main thread (@moyotar)
  • shell-maker #46: Fix byte-compile warnings, runtime cl-lib, defvar ordering and lazy-org declarations (@tothedarktowercame)
  • shell-maker #48: Quote mode-map before splicing into eval'd define-derived-mode (@necaris)

Make it sustainable, sponsor agent-shell

As of this writing, we're 88% of the way to the 150 monthly sponsors goal. If agent-shell is part of your daily workflow, please consider becoming a monthly sponsor (or ask your employer to).

Not in a position to sponsor? A GitHub star helps with exposure and brings in new users.

-1:-- agent-shell 0.87 updates (Post Alvaro Ramirez)--L0--C0--2026-10-13T00:00:00.000Z

James Cherti: org-ibullets.el - Display Emacs org-mode heading stars as UTF-8 bullets

The org-ibullets Emacs package provides a minor mode that enhances org-mode by displaying headings bullets as UTF-8 characters. Features Replace standard Org mode bullets with visually appealing UTF-8 characters. Customizable bullet list for different heading levels. Supports inline tasks with appropriate bullet representation. Configurable face for bullets to match your theme. Installation Installing with use-package and :vc (Emacs version >= 30) To install org-ibullets using the built-in :vc keyword: (use-package org-ibullets :vc (:url "https://github.com/jamescherti/org-ibullets.el" :rev :newest)) :commands org-ibullets-mode :hook (org-mode . org-ibullets-mode)) Install with straight (Emacs version < 30) To install org-ibullets with straight.el: If it hasn't already been done, … Continue reading →
-1:-- org-ibullets.el - Display Emacs org-mode heading stars as UTF-8 bullets (Post James Cherti)--L0--C0--2026-10-12T17:30:47.000Z

James Cherti: wizard.el - A Lightweight suite of useful Emacs functions

The wizard Emacs package provides a collection of useful Emacs functions. The philosophy behind this package is simple: use the built-in functions Emacs already provides to do what complex third-party packages do. Instead of rewriting complex logic from scratch or adding heavy external dependencies, wizard uses built-in Emacs functions to deliver practical features. (For example, to highlighting symbols uses the built-in hi-lock) Features Overview Visuals & Highlighting Symbol highlight: (wizard-toggle-highlight-at-point) toggles highlighting for the symbol under your cursor or the active selection. It is a lightweight alternative to the highlight-symbol package. Native TODO highlighting: (wizard-hl-todo-mode 1) automatically highlights codetags TODO, … Continue reading →
-1:-- wizard.el - A Lightweight suite of useful Emacs functions (Post James Cherti)--L0--C0--2026-10-12T17:19:21.000Z

Sacha Chua: 2026-10-12 Emacs news

People have been having lots of fun playing Emacs Golf over in r/emacs. Check out the "Fun" category for some examples of people comparing how few keystrokes they need in order to get something done.

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

View Org source for this post

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

-1:-- 2026-10-12 Emacs news (Post Sacha Chua)--L0--C0--2026-10-12T11:58:28.000Z

Protesilaos: Emacs ‘denote-data’ testing live today, 2026-10-12, at 16:00 Europe/Athens

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

[ The stream will be recorded. You can watch it later. ]

This evening I will do a live stream where I will continue developing the new denote-data feature for denote: it is an opt-in, in-memory cache with no dependencies that speeds up all sorts of operations.

I have made lots of improvements to it and it is already looking very promising, though I plan to develop it further and eventually write ert tests for it.

I will keep the chat on the side and shall respond to any questions about my programming but also any other issue (the other day I did a 5-hour live stream on the topic of “ask me anything” because my live streams tend to be that, anyway—recording here: https://protesilaos.com/codelog/2026-10-08-ask-me-anything-emacs-tech-politics-life/).

-1:-- Emacs ‘denote-data’ testing live today, 2026-10-12, at 16:00 Europe/Athens (Post Protesilaos)--L0--C0--2026-10-12T00:00:00.000Z

TAONAW - Emacs and Org Mode: Emacs Config Gems - Part 6

I came to realize that attempting to wrap up the “Wordcraft” section (as I call it) in Emacs is a fool’s errand. Emacs is Wordcraft. It’s what it’s all about. By the time I finish a draft, a new config or setting changes what I have. This here is an attempt to stay true to my conclusion in the last section.

Abbrevs

In my opinion, Abbrevs in Emacs is one of its fundamental features. It’s one of those things I’d show someone coming from other word-processing programs to show them the magic of Emacs. In essence, it’s a simple tool: you type two or more letters, and Emacs expands it to a phrase or even a whole paragraph. For example, when you type “hw” it will expand to “hello world” as soon as you hit space after the “w”.

Of course, you can achieve more with this useful function. We will get there. First, let’s discuss general usage:

  • To add a new abbrev, stand with the marker after the abbreviation, and then use: C-x a g for a global abbrev (which applies everywhere in Emacs), or C-x a i l for the specific major mode of the buffer you’re in (so, if you’re working in shell inside Emacs, it will add an abbrev there specifically.)

  • Once added, Emacs has a smart (and when you get it, helpful) way of remembering your lower-case/upper-case usage, depending on how you recorded the abbrev. Assume we’ve added “rnd” for “Research and Development”. This means:

    1. “rnd” automatically expands to Research and Development
    2. “Rnd” expands to Research and development (only the first word is capitalized!)
    3. “RND” expands to RESEARCH AND DEVELOPMENT
  • You can escape abbrevs with C-q after typing the abbrev. If “rnd” expands to “Research and Development” but you actually want to type “rnd” in the buffer, type rnd followed by C-q1.

  • If you want to add a prefix to an abbrev, for example, you want the end result to be “non-Research and Development”, type: non- and then M-’ (this will result in a hyphen showing, which is good) and then rnd to produce the abbrev.

  • Finally, don’t forget about edit-abbrevs which lets you edit your abbrev file directly. This is good when you want to add a bulk of abbreviations (I did this when I wanted to add usernames: I copy-pasted a bunch of those from Outlook one day during lunch).

Alright, with the basics out of the way, let’s dig into the config itself.

First, we want to turn on Emacs’ abbrevs. In my case, I want to put the abbrev file in a folder I sync between my computers so these shortcuts are available on both of them. To do this, I need to set the path for the abbrev file:

(setq-default abbrev-mode t)
(setq abbrev-file-name "~/Sync/Personal/.abbrev_defs")

Now something a bit more advanced. I posted about this before, but I didn’t explain my use case, and I think its usefulness is a bit lost as a result. Here’s the general idea.

At work, we have user IDs (usernames) that I need to use with ServiceNow, our ticketing system. It’s also useful when I want to write an email, and there are five people named “John.” My brain solved this problem years ago with nicknames: the John who has tattoos and a beard looks like a motorcyclist, so I call him Harley (Davidson); the other John loves tea from Georgia, so I call him George, etc.

When I want to dig out Harley’s username, I prefix it with @, and Emacs spits out “jd1234” for his username. This way I don’t hijack the word “Harley” to always be associated with jd1234, only when I really want to use it2.

The post I linked above explains how this works and gives proper credit to Prot, who helped me build this up using regex:

(abbrev-table-put global-abbrev-table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[@].*\\|.*\\)")
(abbrev-table-put org-mode-abbrev-table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[@].*\\|.*\\)")

Grammar Checkers

When it comes to grammar, I explored two options in Emacs. I first started with LanguageTool, then discovered Harper, which I use today. But as these Emacs posts go, I need to take a step back and talk about spelling and grammar checkers first.

With a spell-checking tool, we just need a dictionary: a list of words the program checks against what we typed and identifies the closest match in its list. The mechanism behind it is a mathematical principle of edit distance: it is a fuzzy search for words that are the closest to the misspelled one3. This can be complemented with a personal dictionary of additional unofficial words that we tend to use, and that’s mostly it; we’re good to go.

A grammar checker is a different animal, or actually, animals. And before we start talking about these, it’s worth mentioning that grammar, or what is generally considered “correct” language, is a moving target4. Language is a grey area, where what’s correct can be — at least to a degree — subjective.

The complex mix of tools used in what we cram together today under the term “grammar checkers” is a big underestimation of the history and development of the last 50 years or so of progress and the different tools that we use today. With modern-day LLMs, the confusion extends further, as these usually don’t use any of those tools, relying only on transformers and weights to predict what seems correct and what isn’t5.

Harper

Many Emacs folks, myself included, use Harper these days. It’s no wonder, given its speed and minimal install size, especially compared with LanguageTool, the other free alternative. But this lightness comes at a cost.

Harper provides:

  • a basic spelling checker (dictionary-based)
  • core grammar rule engine

… and that’s it. Indeed, in my first post about Harper, I noted a couple of basic errors it missed, and I still think it’s far from “the private alternative to Grammarly,” as it states on its homepage. I actually barely use it because it’s the competition that does a much better job, grammar-wise. If it were up to me, the tagline would be “a private, fast, customizable grammar checker for when you don’t want a grammar nanny looking over your shoulder.”

Harper is available on homebrew (it seems the folks behind Harper made a dedicated dmg since I last installed it), but for Ubuntu and its kin, it requires Cargo. This also means that if Harper is the only program you have from Cargo, you also need to remember to update it6. I’ve written and explained my Harper setup a few times, but it evolved over time to accommodate both my Mac and my Linux desktop, so I’m including the whole thing here. Note that Harper works through Eglot on Emacs, as I explained in the first post linked above.

(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '(org-mode . ("harper-ls" "--stdio")))) ; This is the eglot-related integration
(setq-default eglot-workspace-configuration
           '(:harper-ls (:dialect "American" :linters (:LongSentences :json-false :AvoidCurses :json-false))))
;; These are options for Harper. I chose American dialect as the language, I also want to ignore long sentence errors (the main issue is that it hides other errors nested in those) and I also want Harper not to tell me when it thinks something is offensive. The full list of these options is in https://writewithharper.com/docs/rules. It needs to be nested inside the :linters option.
(when (eq system-type 'gnu/linux)
  (add-to-list 'exec-path (expand-file-name "~/.cargo/bin"))
 (add-to-list 'exec-path "/snap/bin"))
;; On Linux, harper-ls lives in ~/.cargo/bin (installed via Cargo/Rust).
;; /snap/bin is added for Vale (installed via snap - that's another program I might discuss, ignore it for now) and any future snap packages.
;; On Mac, Harper is installed via Homebrew which handles PATH automatically.

LanguageTool

LanguageTool provides:

In addition, as I was researching more into LanguageTool, I found lesser-known layers and additions that are not fully documented. For example, there’s a neural network for confusing pairs included. I don’t know how important those other tools are, but if you use the full product online to get a taste, you will see the difference when comparing it to Harper immediately.

While it sounds like I’m favoring LanguageTool over Harper, I don’t have it installed anymore, and I haven’t used it in a while. The reason for this is that it requires Java and over 10GB (yep) if you want to get the n-gram running with the other additions. Below is my old config, based on version 6.1. You will need to install langtool.el to run it, without the full layers discussed above (by the time I found out about these options, I stopped using it):

(require 'langtool)
(setq langtool-language-tool-server-jar "~/Path/To/Current/LanguageTool-#.#/languagetool-server.jar")
(setq langtool-default-language "en-US")

All in all, I do not think Emacs is the best place for grammar checkers. YMMV, but since I have access to Grammarly Pro’s features, I prefer it. I also use AI to go through my posts, and it does a few very useful things, grammar being just one of them if I need it to. If I didn’t have Grammarly, I’d consider LanguageTool’s pro package, which comes with additional perks that are not available for a local install.

My logic here is simple: if I write a post for my blog, which is going to be public anyway, I don’t mind using online tools or LLMs. For more sensitive things, like my journal, I use Harper if I want to, but most of the time it’s an overkill for my own mind’s rambling anyway. This is why I said above that I barely use Harper, as I don’t really have a use case for it.


  1. As I’m writing this, I realize another simpler trick is Emacs’ undo. Simply use C-/ (or whatever you have it bound to) and it will cancel the expansion. ↩︎

  2. Now that I use corfu, which also contains my abbrevs, typing “george” will bring up a menu allowing me to choose george from a drop down instead of specifically typing @george. ↩︎

  3. The mathematical concept behind this is called Levenshtein distance, after Vladimir Levenshtein, who came up with this in 1965. The basic idea in spelling programs is to measure how many “jumps” are required to correct a mistyped word. For example, “helo” to “help” is one jump, as well as “helo” to “hello”; “helo” to “hallow” though substitutes e with a, inserts an l, and then inserts the letter w — 3 jumps. This means that pure Levenshtein spell checkers will rank the first two corrections higher on the list (modern spell checkers introduce a few more tweaks). Wikipedia gives us an interesting history lesson here: Unix got Ispell, which is based on SPELL, and GNU built upon that with Aspell, which is what we have in Emacs today. The front end in Emacs, ispell.el, is named after the original program it once used. ↩︎

  4. Brennan wrote a good post about this recently with plenty of links, which I’m stripping here because I want you to go read his whole thing, not just my footnote: “To ask someone to write properly is to assume there is a way to write proper. There isn’t. There is no universal, static version of English. Or any other language, for that matter. Every dialect of English, including regional accents and non-standard forms, follows complex, systematic grammatical rules.” ↩︎

  5. While most popular LLMs today are generic and don’t rely on more traditional grammar tools, specific grammar products like Grammarly and Langtool do use specific traditional tools as well as an LLM on another layer, making the box of grammar checkers as a category even more vague. ↩︎

  6. After realizing how often I forget to update Harper on Linux (I don’t get reminders as I do with other packages), I searched for a solution and discovered topgrade. It’s a Rust tool that probes your system, figures out which packages you use as well as your hardware, and offers you all the updates in one go: apt, flatpak, cargo, snap, and even updates for your boot, if you have a Linux machine from a brand that supports it, like System76. ↩︎

  7. n-gram is a sequence of symbols, or in our case, words, where n is the number of words. So “a nice sunset” is a 3-gram sequence and “they are wearing hats” is a 4-gram one. Grammar checkers look at many of these sequences in a large body of text and evaluate whether what we type has errors. For example, if I write “they are wearing hat,” the checker will run this against several examples and will find that “they are wearing hats” (with an s at the end) is much more common than the first example, which means the first example is probably wrong and should be corrected to what is found in the text. As you can imagine, the checker needs to look at a lot of text to make good corrections, which is why that body of text is huge — hence the size of a few hefty gigabytes. LanguageTool explains n-grams and how they use them, which is helpful. If this sounds similar to LLMs and how they work, it’s because the connection exists: the basic idea of looking through a large body of text is the same, and in a way, LLMs today are built on a similar concept. However, LLMs take this a step further and compute what would make sense using weights and other settings in real time, while n-grams are essentially static lists of words the computer just compares against. There’s no “thinking” involved. It helped me to think about it this way: in terms of hardware, your hard drive reading speed is way more important for n-grams than your GPU. ↩︎

-1:-- Emacs Config Gems - Part 6 (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-10-11T22:54:34.000Z

Irreal: Will The Emacs Keyboard Die?

I’m still under the weather: mostly I’ve been sleeping non-stop except for meals and bathroom breaks. That means that I’m still not up to producing a post with the usual Irreal wit and charm1. I did have this post from Curtis McHale about the future of Emacs interaction in my TODO queue so I decided to do a quick write up about it.

McHale’s contention is that we use keyboards because they’re the best way of interfacing with computers. He points to everybody’s favorite keyboard replacement, voice input, but notes that even on Star Trek—that arguably popularized the idea—Captain Picard still tells his crew members where he wants to go and they type in the commands at their control stations.

As I said in my last post on the matter. my written thoughts are always much better organized than my verbal ones so moving to spoken input would be a net loss for me. Still, it will be interesting to see how things play out.

Footnotes:

1

OK, OK. Maybe that overstates my usual efforts. I blame whatever sickness I have for that.

-1:-- Will The Emacs Keyboard Die? (Post Irreal)--L0--C0--2026-10-11T17:24:04.000Z

D.V.F.P.: Automatic TOC in BLOGGER articles via HTML code

1. Introduction

Every now and then, I use Blogger as a personal system for publishing articles.

The articles are structured with sections and subsections and an initial table of contents.

The articles are mainly written in Org Mode using Emacs.

In this case, the table of contents is generated by the writing system itself during the conversion phase from Org Mode to HTML.

-1:-- Automatic TOC in BLOGGER articles via HTML code (Post D.V.F.P.)--L0--C0--2026-10-11T00:00:00.000Z

Meta Redux: neorepl: A Terminal REPL for Clojure (and Friends)

For more than a decade the Clojure REPL most people saw in a terminal was REPLy, the one behind lein repl. It served us well, but it’s showing its age, and I kept wishing for a terminal REPL that felt like CIDER, without the Emacs part. So I finally built one. neorepl 0.1 is out today!

Why another REPL?

There’s no shortage of Clojure REPLs, so it’s fair to ask. Here’s what I was after:

  • nREPL all the way down. neorepl only speaks nREPL, so it works with anything that runs an nREPL server: Clojure, babashka, shadow-cljs, Basilisp, ClojureCLR and so on. The smarts live on the server. When cider-nrepl is there neorepl uses it, the same way CIDER does, and when it isn’t it falls back to nREPL’s built-in ops, and then to evaluating a bit of code.
  • A REPL that’s pleasant to use: a proper line editor, highlighting, completion, arglists as you type, pretty printing, history per project and a Ctrl-C that actually interrupts the evaluation.
  • A CLI that’s just as good for scripts and coding agents. Every lookup the REPL can do is also a command, with structured output and exit codes that mean something.
  • Something small and modern. Clojure 1.10+, nREPL and JLine 4 are the only dependencies, and the same code runs on the JVM and on babashka. The whole thing is about 3,300 lines right now, and I’d like to keep it that way.

rebel-readline and bb repl --connect are both great, and I borrowed from them freely, but neither tries to be an nREPL-first tool with CIDER’s tooling, jack-in and a CLI for agents. That combination is the whole point of neorepl.

Update (2026-10-11): A few people asked how neorepl relates to the Clojure CLI REPL, which the Clojure team released a couple of weeks ago, and to bb repl --connect. (I’m a bit embarrassed to admit I hadn’t heard of the former until after I announced neorepl.) Funny enough, all three are nREPL clients under the hood, but they’re after pretty different things. In practical terms:

  • The Clojure CLI REPL runs only on the JVM and needs Clojure 1.12+. It ships its own nREPL server, and its docs and data inspector are ops of that server, so against any other server you can only evaluate code. It’s deps.edn only. Its editing is really nice, though - structural editing, evaluating the form at the cursor and a datafy-based inspector - and I’ll be borrowing some ideas from it. It doesn’t do completion yet.
  • bb repl --connect puts babashka’s console REPL in front of any nREPL server, with completion, arglists and docs, in very little code. It’s a great way to poke at a running server, but it won’t start one for you.
  • neorepl works with any nREPL server and gets the most out of the ones with cider-nrepl (or babashka’s take on it), without any middleware of its own. It starts a server for Leiningen, deps.edn, babashka and shadow-cljs projects, follows you into ClojureScript, and everything the REPL does is also a command with JSON output for scripts and agents. No structural editing yet, however.

So, if you want the fanciest line editing and you live in deps.edn land, give the Clojure CLI REPL a try. If you want CIDER in your terminal, for you and for your agents, that’s what neorepl is for. The neat part is that babashka’s server bundles CIDER’s inspector and test runner ops these days, so neorepl’s test runner (already on main) and the inspector that’s coming next work with babashka out of the box.

For humans

Here’s the REPL in action:

The neorepl REPL

A few things worth pointing out:

  • Enter only submits the input once its forms are complete, so pasting a whole function just works.
  • TAB completes, with the candidates’ types next to them, and the arglist of the function you’re calling shows up after the cursor.
  • Values get pretty printed on the server and highlighted on the client.
  • Commands start with a comma, the CIDER way: ,doc, ,source, ,apropos, ,macroexpand, ,pst, ,reload, ,edit (which opens a definition in your $EDITOR) and ,help for the rest. The Clojure reader sees a comma as whitespace, so they can’t clash with real code.
  • Ctrl-C interrupts what’s running, and code that reads *in* gets its input, something REPLy never quite got right.

Without a server to connect to, neorepl starts one for the project, the way CIDER’s jack-in does, with cider-nrepl on the classpath, and stops it when you exit. It knows Leiningen, the Clojure CLI, babashka and shadow-cljs projects.

ClojureScript works too. Start a ClojureScript REPL in the session (through shadow-cljs or piggieback, which jack-in adds for you) and neorepl follows along: evaluation, completion, docs and the rest switch to ClojureScript, and :cljs/quit takes you back.

For agents

Coding agents are surprisingly good at Clojure, as long as they have a REPL to talk to. Most of them drive CLIs much better than anything else, so neorepl has a CLI that’s meant to be driven:

neorepl's CLI

  • neorepl eval evaluates code on the project’s server. Calls share a session, so *ns*, *1 and *e carry over from one call to the next, and --session keeps separate lines of work apart.
  • --format json (or edn) gives you the values, the output and the namespace as data.
  • --timeout interrupts the evaluation before giving up, so an infinite loop doesn’t take the server down with it, and --max-output keeps a stray (range) from eating the agent’s context.
  • neorepl check finds unbalanced parens, brackets and braces without a server. That’s the most common way for an agent (or a human) to break a Clojure file.
  • neorepl doc, source, apropos, complete, where, macroexpand and reload do what the REPL’s commands do.
  • neorepl server starts a server that stays up, for editors, agents and neorepl eval to share.

The exit codes are boring on purpose: 0 when all went well, 1 for an evaluation error, 3 when there’s no server to talk to and 124 for a timeout (like timeout(1)).

Playing nicely with nREPL

A new client is also a new way to break servers, or to be broken by them, and the nREPL world has quite a few servers these days. Fortunately I’ve been working on proof, a compatibility suite for nREPL, which made neorepl a good test subject.

proof checks both sides of the conversation. proof proxy sits between a client and a server and grades everything the client sends: every request has an id, need-input gets answered with stdin in the right session, and so on. proof serve is a small nREPL server that can behave like other servers in very specific ways - sending only the last value of the code like Basilisp and jank, dropping stderr, having no interrupt op, splitting output into one message per character, and so on. I ran neorepl’s REPL and CLI through both, against nREPL, babashka, Basilisp and jank, and through every quirk proof serve knows about.

neorepl came out of it in decent shape, but not unscathed:

  • Code read from stdin (neorepl eval -) that then read *in* itself crashed neorepl and left the evaluation waiting on the server for input that was never coming.
  • neorepl’s errors could leave the shell’s prompt dangling at the end of a line with servers that don’t end their error reports with a newline (hello, jank!).
  • neorepl eval exited with 1 when it lost the server, which is the status for code that failed.

All of those are fixed in 0.1. It also turned up a couple of things that are nREPL’s fault rather than neorepl’s: need-input and the end of an interrupted evaluation can go to the wrong connection when a session is used from more than one. Those will be fixed in nREPL itself.

It works the other way around too. proof now checks servers with the exact requests neorepl sends while starting its REPL and evaluating code, next to the ones CIDER, Calva, Conjure, vim-fireplace and REPLy send, so a server that breaks neorepl finds out from proof, not from a bug report. nREPL, babashka, Basilisp and jank all pass those checks today.

Making the neighbours better

Building neorepl turned up a few problems elsewhere, which is one of my favourite side effects of writing a new client. piggieback used to evaluate only the first form you sent it and silently drop the rest, and it loaded the ClojureScript compiler at startup even when nobody asked for a ClojureScript REPL, which added almost half a second to the startup of every server with ClojureScript on the classpath. The fixes for both (piggieback#162 and piggieback#161) will be in the next piggieback release. The Node.js REPL in ClojureScript itself doesn’t cope well with interrupted evaluations, and that one is next on my list.

What’s next

0.1 is the foundation. Here’s what I’d like to tackle next:

  • a test runner with proper reports
  • a full-screen inspector and stacktrace browser
  • an MCP server and a ready-made skill for coding agents
  • better Windows support

Try it

The easiest way to install neorepl is bbin:

$ bbin install io.github.nrepl/neorepl
$ cd my-project
$ neorepl

The README has the details.

It’s early days, so I’m sure you’ll find rough edges. Please file issues, and tell me what you’d like to see next. And if you’re one of the people who kept lein repl and REPLy going over the years - thank you! neorepl wouldn’t exist without you.

Keep hacking!

-1:-- neorepl: A Terminal REPL for Clojure (and Friends) (Post Meta Redux)--L0--C0--2026-10-10T09:38:00.000Z

Irreal: Still More On Entering Ranges

The topic that wouldn’t die. It started with a quick post on Sacha’s solution to a problem posed by David Pape. I thought it was a nice solution but then 0-_-0_ pointed out that abo-abo had already solved this problem with his tiny library. So I wrote about that and prepared to move on.

I was not allowed to. Frequent commenter Phil remarked in my original post that he would use a keyboard macro. He suggested that that would make sense for those of us who who seldom need to enter a sequence of ranges like Pape. In Pape’s case this was something that he needed to do a lot so a solution like Sacha’s or abo-abo’s make sense. I agree with Phil. A keyboard macro is exactly how I would solve this problem given that I seldom have it.

This seems pretty obvious and might not be worth writing about except that Phil’s solution showed me something that I didn’t know: You can set the initial value of the counter with a Ctrl+u n where n is the desired initial value. That’s a lot easier than using Ctrl+x Ctrl+k Ctrl+c to do the job.

As a final note, I think that Phil had a typo in his macro. As far as I can tell it should be

C-u 47 <f3> # <f3> , SPC <f4> <f4> <f4> <f4> <f4> <f4> <f4> <f4>

At least that’s what works for me.

It turns out that I regularly use a macro with a counter so I’m really happy to learn that the counter can be set so easily.

Update [2026-10-10 Sat 12:12]: Fixed link.

-1:-- Still More On Entering Ranges (Post Irreal)--L0--C0--2026-10-09T15:34:21.000Z

Irreal: More On Number Ranges

In a nice coda to yesterday’s post on Sacha Chua’s solution for entering number ranges, 0-_-0_ points out that a solution already exists. That solution is abo-abo’s tiny library. I vaguely remember seeing tiny when abo-abo released it 13 years ago but I never followed up and used it. Still, anything from abo-abo is worth taking a look at and, in any event, we now have two solutions to the problem.

Abo-abo’s solution brings to mind another Emacs lesson: very often, if you need some functionality, there’s a good chance that it already exists. Of course, that’s not the same as being easily discoverable. An internal function can usually be discovered through the on-line documentation but a third party library like tiny won’t appear in that documentation until it’s loaded.

That’s it for today. I just wanted to tell the rest of the story from yesterday.

-1:-- More On Number Ranges (Post Irreal)--L0--C0--2026-10-08T15:33:30.000Z

James Cherti: Fixing the Emacs 31+ Tree-sitter syntax highlighting issue in indirect buffers (The Emacs maintainers will not fix it, so I did)

In Emacs 31+, an issue appeared where creating an indirect buffer from a base buffer running a tree-sitter major mode results in a loss of syntax highlighting. I reported this issue, but the Emacs maintainers will not fix it (hopefully, they will see the benefit of fixing it one day, once they find a way to fix the architecture). This means that users who depend on indirect buffers are left without a solution. To address this, I wrote a workaround that fixes syntax highlighting in cloned indirect buffers that use tree-sitter in Emacs >= 31. (See below.) Note: I published … Continue reading →
-1:-- Fixing the Emacs 31+ Tree-sitter syntax highlighting issue in indirect buffers (The Emacs maintainers will not fix it, so I did) (Post James Cherti)--L0--C0--2026-10-08T12:45:56.000Z

James Dyer: Org Speed Keys: Making t Land After the TODO Keyword

I use org-use-speed-commands all the time when navigating and performing actions on org headlines, but I think I have found a friction point, something that just feels a little off.

When the point is on * and then you press the key t, it calls org-todo via org-self-insert-command. Fast, but the feel is off, the point stays on the stars, so I have to move right before I can type the headline.

20261008110300-emacs--Org-Speed-Keys-Making-t-Land-After-the-TODO-Keyword.jpg

The reason is in org-todo itself. It has a cursor fixup that parks point just after the keyword, but only when point is not at beginning of line. Speed keys by definition run at beginning of line (org-speed-command-activate requires (bolp)), so the fixup never fires for exactly the keys that would benefit most.

There is no custom option for it, so I added a small :after advice:

(defun my/org-todo-stop-after-keyword (&rest _)
  "Move point to just after TODO keyword when `org-todo' leaves it at BOL.
Speed command `t' (via `org-self-insert-command') runs with point at
beginning of headline, where `org-todo' skips its cursor fixup."
  (when (and (bolp) (org-at-heading-p))
    (when (looking-at org-todo-line-regexp)
      (goto-char (or (match-end 2) (match-end 1)))
      (when (looking-at " ") (forward-char 1)))))
(advice-add 'org-todo :after #'my/org-todo-stop-after-keyword)

Now * Test with point on * plus t becomes * TODO Test with point on Test, ready to type. Same for cycling * TODO to * DONE.

Small change, but speed keys finally feel like they finish the job instead of leaving me parked on the margin!

-1:-- Org Speed Keys: Making t Land After the TODO Keyword (Post James Dyer)--L0--C0--2026-10-08T10:03:00.000Z

Bicycle for Your Mind: Using Gravity For Your Thoughts

gravitygravity

Product: Gravity
Price: $9.99

The Problem

We are all familiar with the act of note-taking. A thought occurs to you, you switch to your note-taking application and face a slew of decisions:

  • What are we going to call this note?
  • What are we going to tag this note? Or which folder does this note belong to?
  • How does this fit into my existing tag-system? What was the tag I used a few months ago on a similar note? It belongs to this folder, but also belongs to this other folder, do I alias this note to the other folder?
  • Which note is this new note related to? Do I need to link them? What about back links?
  • And on and on and on.

Our note-taking systems have evolved into a quagmire. We are now in the business of managing our systems. Instead of taking notes, we work incessantly on our note-taking system. Soon we avoid taking notes, it is too much bother.

Look at the discussions on the Zettelkasten Forum:

Or go to Obsidian Forum. You will see the same discourse. People obsessing over the system.

A Perspective

I had heard of Andrej Karpathy. Couldn’t tell you in what context, but the name was familiar to me. He wrote the following: The append-and-review note – karpathy.

Go read it, it is a small read. I will wait.

His system is two steps: Append and Review. Append to a single file. No tags, no metadata of any kind. This is the line which stood out to me: “When I note something down, I feel that I can immediately move on, wipe my working memory, and focus fully on something else at that time. I have confidence that I’ll be able to revisit that idea later during review and process it when I have more time.”

He periodically reviews his notes moving things which are important at the moment to the top. The rest of the information continues to sink to the bottom, like gravity.

The main advantage of this system? No friction. No decisions at the moment of entering a note/thought. Nothing lost. Nothing falls through the cracks.

Enter Gravity

Gravity is an attempt to provide an environment for this system to work. It can be a menu-bar only application. You press a keyboard command wherever you are. Up pops an editor window. You type or paste in what your thought was. Hit ⌘↵ or three ↵’s. The note is saved. You are back to the program you were in initially.

Periodically you can review your notes. The ones which are more relevant you can nudge up the chain. Everything else keeps getting demoted and drops down the list, like gravity.

Gravity is powered by a SQL database as its back end. You can export your whole repository of notes to a Markdown file, a text file, or a JSON file. So even though it is a database, there is no lock in to the product.

I was intrigued by the following line from the developer, Tristan Manchester:

Gravity has one job: minimize the time between “I have a thought” and “the thought is captured and I can move on.”

That appealed to me and I got curious.

Gravity The Program

gravity - windowgravity - window

A keyboard command, you can customize, from anywhere and this is the Gravity window. All of your thoughts and their content in a window.

gravity - capturegravity - capture

This is the capture window. A keyboard command, customisable, and this window pops up. You enter your thoughts, hit the return key twice, or ⌘+↵ and the content is saved to Gravity. You hit ⇧+↵ key to add a new line. (I notice that a simple ↵ does add a new line too).

In the Gravity window, you have the choice of promoting any entries you need to the top of the list. If you have a long list, this makes items which are relevant to you be readily accessible and it is the premise behind the program. Items which are not relevant for the time being keep sinking to the bottom.

You have the option of archiving or deleting thoughts you want to keep or want to discard.

It is all done in Gravity.

Gravity Preferences

They are well thought out and well designed.

pref - generalpref - general

You will want Gravity to launch at login. You want it to be always available. I don’t much care for the program to show up in the Dock. You can choose to have the two ↵’s to save the capture or rely on the usual ⌘+↵ command. I like having both.

pref - keyboard 1pref - keyboard 1

pref - keyboard 1pref - keyboard 1

pref - keyboard 1pref - keyboard 1

The program is fully keyboard driven. I love this aspect of the program and they are all customisable. You set your own or stick to the ones shipping with the program. Regular readers know how much I like this feature.

pref - syncingpref - syncing

You can choose to sync to iCloud. That allows the same document in multiple machines. I love it.

pref - searchpref - search

You can choose to have the archives included in the search. If you have a long list of entries in the current focus and want to move some over to the archive, you can still include them in the search and nothing is left out of your searches.

pref - advancedpref - advanced

This is where you can export your whole collection of notes to a Markdown file, a plain text file or a JSON file.

pref - more advancedpref - more advanced

You can monitor some stats about your usage of the program. Optimize your SQL database. There is also a command line tool (I haven’t explored this much. Although I have installed it).

How Well Does Gravity Work?

This is from the developer of Gravity:

I get the same feature request about once a week. “Love Gravity, but could you add folders?” “Would be perfect if I could tag notes.” “Any plans for linking between notes?” “An API would be amazing.” The answer to all of these is no. Not “not yet”. Just no.

This probably sounds like bad product thinking. Aren’t you supposed to listen to users? Give them what they ask for? Iterate toward their needs?

Usually, yes. But there’s a specific reason Gravity works the way it works, and adding these features would break it. The limitations aren’t gaps. They’re the design.

Someone recently asked me why Gravity doesn’t have folders. My first instinct was to say “we’ll add that eventually.” Then I caught myself. We’re not adding folders. We’re not adding tags either, or bidirectional links, or a plugin system, or templates, or a knowledge graph. These aren’t items on a roadmap we haven’t gotten to yet. They’re deliberate omissions. The missing features are the product.

I found this interesting. Mind bending, but interesting. Then I remembered:

Gravity has one job: minimize the time between “I have a thought” and “the thought is captured and I can move on.”

If you are looking for a capture application which is designed to do just that, have a thought, capture a thought, move on with life, then Gravity is the ideal tool for that need.

It is well-designed software which does that and does that well.

Is That All I Need From a Capture Application?

I am old. I forget things. There are too many times during the day when I have a thought and it floats away a few minutes later. For instance, read the Org manual and read the section on narrow and widen. A few minutes later, I have no recall of what I was thinking about. My solution? Capture the thought. Do I want to capture the thought in a stream of thoughts? Or do I want to categorize the thought into a file?

The program I use for this is Capture for Mac. I have a text file which has markdown headings like:

  • Ephimeral
  • Emacs
  • Watch Later
  • Read Later
  • Programs Installed
  • And so on.

The Capture window in Capture gives me the option of inserting the content under a specific heading. It adds a time stamp and some context if I have highlighted something. That is what I use for capture at this point.

You can follow a similar arrangement in Gravity. Prepend some text to the capture. For instance, things about Emacs, can have Emacs: to start with, or Read: for things to read later or Watch: to watch later, and so on. And the system works because the Search function in Gravity is excellent.

I understand that the goals of the two programs are different. They are not solving the same problem. If your need is to record thoughts and move on with your life, Gravity as a system is perfect for that. I love working in it. I admire what it is trying to do and feel it is perfectly designed for that.

Of Course, Emacs

("o" "OBTF" entry
           (file+headline "~/Dropbox/org/obtf.org" "OBTF")
           "Created: [%(format-time-string \"%Y-%m-%d %H:%M\")]\n %?"
           :prepend t
           :empty-lines-before 1
           :empty-lines-after 1)

You know I have to do everything in Emacs. I added this Org-capture template which lets me perform a similar function in Emacs. I can capture content to a file called OBTF.org. I can then refile content to the top or the bottom of the file through danielsz/Palimpsest. I can have an Archive heading and move sections to it through org-refile. All of this is more effort than Gravity, but it has the added advantage of being in Emacs.

Some More Reading

Blog — Thinking About Capture — Gravity

No, We’re Not Adding Folders — Gravity

Conclusion

Gravity is a fantastic application to capture your thoughts. It is a product with a defined philosophy and it sticks to its vision.

I recommend Gravity heartily for your capture needs.

macosxguru at the gmail thingie.

Note: Thanks to Tristan Manchester, the developer of Gravity, for providing me with a promo code when I asked for one.

-1:-- Using Gravity For Your Thoughts (Post Bicycle for Your Mind)--L0--C0--2026-10-08T03:52:01.000Z

Protesilaos: Live: Ask me ANYTHING (2026-10-10 16:00 Europe/Athens)

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

[ The stream will be recorded. You can watch it later. ]

This Saturday, 10 October 2026, at 16:00 Europe/Athens time, I will do an “ask me anything” live stream. I have been meaning to do this for a long time, but I never found the chance for it.

Ask me a question about anything—literally ANYTHING—and I will give you an honest answer (e.g. Emacs, philosophy, technology, keyboards, politics, the hut, Français, and more!). It does not matter if the question is personal or sensitive. I will give you a complete answer to the best of my abilities. If you need any ideas, check the rest of my website, where I write at length on a broad range of topics.

The plan is to do a live stream for at least 1 hour. If there are many questions, I will extend it to 2-3 hour or, generally, for as long as needed. And if there are no questions, then I can do something related to Emacs or close the stream earlier—no problem!

-1:-- Live: Ask me ANYTHING (2026-10-10 16:00 Europe/Athens) (Post Protesilaos)--L0--C0--2026-10-08T00:00:00.000Z

Sacha Chua: Des commandes vocales pour ajouter des tâches sur Emacs

Liens :

0:00 Introduction

Je veux vous présenter ma petite expérience de la reconnaissance vocale pour dicter dans Emacs et pour les contrôler. J'utilise le modèle Orukeet pour la reconnaissance vocale parce qu'il est assez rapide et assez précis. Je l'intègre dans Emacs en utilisant mes bibliothèques sachac/whisper.el et speech-input. J'ai récemment créé des commandes vocales pour ajouter des éléments à ma liste de tâches. C'est étonnement utile parce que ça réduit la charge cognitive liée à la mémorisation des raccourcis clavier. Je vais vous montrer cela.

0:37 Ajouter des tâches

image from video Je presse la touche zéro du pavé numérique avant et après avoir dit « Rappelle-moi demain de préparer une présentation pour Emacs Paris sur les commandes vocales. » La fonction crée une tâche dans ma boîte de réception. La tâche est planifiée pour demain. Il y a un lien vers l'enregistrement

1:09 D'autres dates

image from video « Rappelez-moi demain de préparer une présentation pour Emacs Paris sur les commandes vocales », la capture d'écran et le contexte de tâche. Ça m'aident si j'ai été mal comprise ou si j'ai besoin de contexte. De la même manière, je peux dire « Rappelle-moi la semaine prochaine de faire la paperasse pour l'école. » « Rappelle-moi aujourd'hui de créer une vidéo sur mes commandes vocales. »

1:59 Basculer à une tâche

image from video Je peux aussi créer une tâche et déclencher le suivi du temps (ou clock in). Par exemple, je peux dire « Commence à implémenter la génération des questions. »

2:15 Retrouver le fil de mes pensées

image from video Si je perds le fil de mes pensées : « j'en étais où ? » Ça m'affiche les tâches récentes.

2:27 Je faisais quoi ?

image from video « Je faisais quoi ? » Ça bascule sur la tâche actuelle. Voilà des commandes vocales pour ajouter des tâches.

View Org source for this post

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

-1:-- Des commandes vocales pour ajouter des tâches sur Emacs (Post Sacha Chua)--L0--C0--2026-10-07T19:52:45.000Z

Curtis McHale: Emacs Carnival - Will we change how we interact with Emacs?

Will voice kill the keyboard. No. Will the way we interact with Emacs, and computers in general, change drastically? No.

Star Trek has a mythical AI/computer that responds to voice commands and interacts with you to clarify or note other work that may pertain to what you wanted. Evidently the interface is so ubiquitous that Scotty talks to a mouse noting it's "quaint" to need to push it and type on a keyboard to interact with a computer.

But if we watch the bridge of any Star Trek show people aren't talking the whole time. They're tapping screens to get the computer to do things for them. Theoretically the captain could tell the ship where he wants to go and let the computer do it, but he issues a command and then someone types in the needed commands to the ship so it goes in the expected direction.

I work at home in a solo office space. I could talk to my computer and it would disturb no one, except me. I'd be unable to listen to music on my stereo at the same time just in case the computer couldn't understand my words versus the song.

I don't think keyboards are going anywhere any time soon. They provide an unambiguous interface to your computer that doesn't disturb others. I think that those loving Emacs interactions like they use it will continue to do so as they do now because it's the most efficient way to control the Emacs interface.

-1:-- Emacs Carnival - Will we change how we interact with Emacs? (Post Curtis McHale)--L0--C0--2026-10-07T17:55:00.000Z

Irreal: Inserting Number Ranges As A List

The incomparable Sacha Chua has a very nice solution to a problem posed by David Pape: how can we enter a range of numbers as list of distinct numbers in Emacs? Pape’s specific problem was entering the numbers of error reports that a Git commit closes. Rather than entering something like

closes: 47, 48, 49, 50, 51, 70

he’d like to enter something like

closes: 47-51 70

and call an Emacs function to have it expanded into the first example.

Chua has a very nice solution that covers that case as well things like

47-51, 70

or even

5-15/2

to expand into the sequence

5, 7, 9, 11, 13, 15

Most of the heavy lifting is done by the number-sequence function. The rest of the work is parsing the input string into either a single number or a range of numbers that number-sequence can work on.

Chua’s solution strikes me as an example of Emacs at its best. The problem it solves is not earth shattering. It’s simply a bit of friction in a user’s everyday workflow but Emacs easily provides a solution that makes the friction disappear. Take a look at her code. It not only shows an ingenious solution to the problem but also demonstrates the power of Emacs and Elisp to make the application of that genius simple.

-1:-- Inserting Number Ranges As A List (Post Irreal)--L0--C0--2026-10-07T15:40:09.000Z

Sacha Chua: Emacs Carnival Oct 2026: Exploring keys and voice input in Emacs

Making more use of the keyboard

October's Emacs Carnival theme "Are keyboards and M-x here to stay?" is a good nudge to think about the voice interface I've been experimenting with and where I want to take it.

Like Chris Maiorana, I love the way Emacs's M-x (execute-extended-command) allows me to call any command by name. It means that I can call commands or come up with my own commands without worrying about assigning them to keyboard shortcuts or making sense of where to place them in a hierarchical menu. With flexible completion via the vertico and orderless packages, I don’t even need to type in the full name of the command or remember the words in the right order. If I still can’t remember the command, it’s easy enough to use defalias to make my own nickname for it.

2026-10-06_11-04-07.png
Figure 1: I can filter M-x to show just my custom commands

I tend to limit keyboard shortcuts to only my most common commands. Everything else is hard to practise enough so that it sticks in my muscle memory. Even with marginalia showing me keybindings when I use M-x and which-key showing me hints when I pause, it doesn’t sink in.

2026-10-06_11-05-00.png
Figure 2: Marginalia shows keybindings

Maybe I could use key-quiz to drill a small number of keyboard shortcuts I’m actively working on learning, or modify help-quick-sections to show me reminders when I use C-h C-q (help-quick-toggle).

Getting one keyboard shortcut to do the right thing in different contexts is great because it means fewer shortcuts to memorize. Sometimes I can do that by binding the right functions to different mode maps with bind-key, like the way my sacha-org-insert-link-dwim uses the right link syntax in Markdown or OddMuse too. I also like the context-sensitive keyboard shortcuts I get from Embark. I find myself using a handful of those regularly, and C-. (embark-act) starts a bunch of shortcuts across different types of things: i to insert, w to copy, and so on. For example, I know C-x C-f (find-file) will let me select a file to open. Instead of opening it, I can use C-. i to insert the filename into the buffer.

Simpler keyboard shortcuts help. I rarely use the numpad on my laptop keyboard, so those keys can be mapped to functions that don't need to be followed by lots of typing. For example, 0 is a large button. I use it to start and stop speech recognition. Actually, I bind f9 in Emacs to sacha-whisper-run-at-point, and I bind numpad-0 as a keyboard shortcut in KDE (my window manager) instead of just in Emacs so that it's available globally. I'm still on X11 instead of Wayland. The shell script xdotool-emacs focuses Emacs briefly and sends the keys, and the shortcut is set to /home/sacha/bin/xdotool-emacs key --clearmodifiers F9. I do that instead of using emacsclient because emacsclient starts a different buffer, and I might want to keep the context. My sacha-whisper-maybe-type function detects if Emacs is not the focused application and types the text if it isn't, which means I can use it as keyboard input for oher applications while still taking advantage of the corrections and commands I've defined in Emacs. An easy-to-press global shortcut means I can use it from anywhere, even when my foreground app is the browser or another application.

An additional modifier opens up more possibilities for keyboard shortcuts near the regular typing position. My Windows key acts like a Super modifier. I've set it up as a one-shot modifier with Kanata so that I can either hold it down to access more shortcuts or tap it to treat it like a leader key.

There are quite a few commands for which neither M-x nor keyboard shortcuts feel like the right fit: things I want to do faster or more fluidly than M-x, but which I don't do often enough to memorize a good shortcut for. I've been experimenting a bit with voice input, and I really like it.

Some reasons why I'm curious about speech recognition

  • I could use more practice speaking, since I don't talk to a lot of people in real life.
    • English: Videos and livestreams can be a way to share interesting things and start conversations. I want to organize my thoughts, improve my breathing, and reduce my stutters and false starts. That will make the audio more pleasant to listen to. Dictating to my computer gives me more practice. It also makes things easier to livestream if I can reuse my speech as input.
    • French: I like challenging my brain and I want to help the kiddo learn it too. Improving my speaking will also make it easier for me to listen to myself and close the feedback loop. Maybe I can even use speech recognition to give me feedback and increase motivation while speaking, like keeping track of words or phrases I want to use. When I use them in conversation, the buffer can cross them off the list, which lets me focus on the words remaining.

      2026-10-07_07-33-03.png
      Figure 3: Screenshot showing balance and target words
  • Dictation can capture ideas quickly, with more natural wording, especially since typing sometimes gets messed up on my computer.
  • Voice interfaces might let me reduce some kinds of task friction or work hands-free. For example, I like setting timers by voice on my phone while cooking. It might be interesting to be able to dictate a draft while knitting or hand-sewing.
  • Brain fog is something I will likely need to deal with. I got a preview of it thanks to the sleep deprivation of early parenting. I know that brain fog is probably going to happen around menopause, it will probably also be part of aging, and it's a common symptom of long COVID which I might still end up getting despite my precautions. I could get into an accident and hurt my hands. I like Emacs and I want to be able to keep using it even if cognitive or physical changes make it harder to remember or use lots of keyboard shortcuts. If I start playing around with this now, the capabilities and the habits will be there when I need it.
  • Other people are already dealing with those kinds of physical or cognitive constraints, so things I figure out might be useful for other people.
  • A global shortcut for voice notes and commands let me quickly dash off a reminder before I take off. I don't have to find Emacs and go through my Org capture templates.
  • It's fun. I like saying "Prepare for take-off" and having Emacs get ready for a spontaneous stream by updating YouTube using my current Org Mode subtree title and description. I like seeing if my dictionary of substitutions can fix the commonly misrecognized words. Saying "I command you to…" to get Emacs to do a M-x command is totally inefficient but also playful, and silly things stick in my brain better thanks to the mnemonic advantage of humor. My brain likes that kind of novelty.

For context, I use Linux on a Lenovo P52 laptop (manufactured 2018, 64 GB RAM but hardly any GPU), and I have an Android phone. I like writing regular text, Emacs Lisp, and some Python and JavaScript. I'm slowly starting to have more focused time now that the kiddo is in virtual school, although it's still broken up by recess and lunch. I will probably have an increasing amount of focused time over the next few years.

Quick benefits from voice commands

I love the way voice reduces context shifts. For example, some of my most-used voice commands are:

  • remind me [today/tomorrow/next week/next month/someday] to…: capture and schedule the task; include an Org link and screenshot for context, and link to the audio recording in case the text was misrecognized

    2026-10-07_10-00-55.png
    Figure 4: Captured tasks have an audio recording, a screenshot, and a link
  • switch to…: capture the task and immediately clock into it
  • what was I doing?: jump to the current task
  • where was I?: show recently-clocked tasks

    2026-10-07_10-02-00.png
    Figure 5: This is the same as C-u M-x org-clock-goto

I don't have to think about which keys I've configured in org-capture-templates. I just use a natural phrase.

I've defined "open …" to open things from my bookmarked links stored in an Org file. Because my numpad-0 keybinding works from anywhere, I can even use it to open sites or capture notes while looking at my browser.

Because Emacs can interact with lots of other applications, I can even experiment with controlling those applications by voice. For example, I use obs-websocket.el to control OBS for streaming or recording a video. Using "Let's go live" and "Over and out" to start or stop streaming or "Start recording" and "Stop recording" to manage the recording means I can do that without needing to find the right window. I stream with a 10-second delay so that there's a little bit of time to catch any private information that's been accidentally shared on the screen. My new "panic" command lets me discard that delay and interrupt the stream by using an OBS keyboard shortcut to kill the stream immediately, even if I'm probably not going to remember that exact keyboard shortcut when I'm stressed. It should be a little easier to remember that than a keyboard shortcut I hardly ever use or a menu item that is a really small target (and my Emacs might not even be my foreground app).

For regular dictation, I'm experimenting with having it automatically place each sentence on a new line or list item. This makes it easy to reorganize in Org. I can refill it into a paragraph when I'm done.

I still have to deal with the limits of my memory, of course. I have a "what can I say" command that opens up an Org file with my notes. I keep this as a manual note instead of just dumping the contents of my sacha-whisper-commands variable so that I can prioritize the things I'm trying to build as habits.

Here's the part of my config that sets up some speech commands.

I might want more complex grammar someday for things that regular expressions can't easily handle. That's something other projects have done a good job of figuring out.

Prior work on voice control of Emacs: Voicemacs, Emacslisten

There's been a lot of good work on voice input over the years, especially since a number of Emacs users have a hard time with or would like to avoid repetitive stress injury. In addition to the innumerable projects that let you use Whisper or similar speech recognition engines to dictate prose, there are also a few projects that use a constrained vocabulary to more reliably control Emacs. Two projects in this area are based on the Talon voice control system: Voicemacs and Emacslisten.

  • The Talon community hub defines some Emacs commands. I think it simulates keyboard input and assumes that you have flx or some other fuzzy completion matching so that input like M-x t-d-on-e runs toggle-debug-on-error, as defined in community/apps/emacs/emacs_commands.csv. I seem to be having a hard time with getting orderless to do that with orderless-flex, so I'm just going to remove that column of the CSV from my emacs_commands.csv.
  • Emacslisten uses Emacs Lisp macros to define the command vocabulary. Cursorfree defines a whole bunch of them, using hatty for voice navigation within a buffer.
  • Voicemacs defines its commands in Python in the user's Talon config (see example). It feels fairly complex, like it assumes it's being cloned as your ~/.talon/user directory. It lets Emacs and Talon communicate via RPC to send additional context. It also extends some things like dired and company to make them more voice-friendly.

I'm new to both projects, so I'm not sure if I'm reading this right, but I think I might be able to use Emacslisten to define voice commands with Emacs Lisp, and then maybe someday use Voicemacs to provide richer context for some of those commands. For example, Voicemacs can list snippets, but I'm not quite sure how to use that yet.

It looks like I can use Talon for commands and my whisper.el setup for regular dictation at the same time. The Pipewire virtual sinks help with that, I think, since the programs don't have to have an exclusive lock on the audio device. This means that I can explore the kinds of vocabularies that people have set up while also taking advantage of greater accuracy and ease of correction in my whisper dictation workflow. I think I can use the Talon process socket to turn off recognition when I want to dictate using my whisper.el setup, and then I can turn it back on afterwards.

A note about the underlying speech recognition: Talon is cross-platform and free, but not open source. Subscribing to Talon (CAD 36/month) lets you use a Conformer + Whisper hybrid engine that uses whisper-large for more accuracy, but that's a bit on the expensive side for me. Dragonfly is a free open source engine that can use Kaldi or CMU Pocket Sphinx for speech recognition, but I'm not sure what the performance is like.

Interestingly, because Talon simulates keystrokes, using the commands from the community set means keys sometimes arrive out of order on my computer. "split solo" is supposed to run M-x delete-other-windows, but it sometimes ends up sending M-x delete-other-wnidows instead. This is the same sort of annoyance that gets in the way of typing quickly on my computer. Keystrokes get processed out of order when the CPU gets busy, which is part of the reason why I find dictation to be more reliable than typing sometimes. I should fix that someday, but my attempts so far aren't working. (I wonder if it has something to do with the GPU under Linux; maybe I should drop back down to CPU only since it turns out I can't use the GPU for much anyway.) Quick fix, let's try adding a brief delay in my settings.talon:

settings():
    # Adds a small pause (in ms) between each character typed
    insert_wait = 5

    # Ensures modifiers (Shift/Ctrl) don't un-latch too fast
    key_wait = 1.0

I think Voicemacs uses a sevver and and Emacslisten uses emacsclient to communicate with Emacs directly, so they probably won't run into this keystroke problem.

It might be interesting to see if I can port some of my simpler commands over to Emacslisten or straight Talon so that they're always active. Then I can slowly work on getting used to the other commands people find useful, or incorporating them into my setup.

Imagining a hybrid approach: keyboard, voice, even mouse/stylus?

I love shifting fluidly from one form of input to another, depending on what makes sense at that moment.

For example, I often start my thoughts by drawing on a light dotted grid using Noteful on my iPad. That lets me quickly cluster ideas spatially, drawing connections and highlighting words as I figure out what I want to say.

Then I use dictation and typing to sketch the outline, jumping around as thoughts occur to me. If I'm livestreaming at the same time, that helps me demonstrate things along the way and lets me benefit from people's questions. Dictation lets me explain things to people and capture the text at the same time.

I can move around with the keyboard, but it would be really cool to use voice for quick navigation between sections of my outline using either line numbers or words. I've implemented a "line" command that lets me jump to a line using the last two digits of its line number, since I probably won't have more than 100 lines visible on the screen at a time. For example, this line is currently 119, so referring to it as "nineteen" would work. I'd like to add more operations for editing the text. I think it would be great to be able to say "kill line 33" to delete that line or "move lines 16 to 20 after 27" to have it move those lines there.

I now have a sacha-whisper-one-sentence-per-line variable that splits up dictation if non-nil, so we'll see where those take me. Orukeet does a reasonable job of punctuating my sentences if I don't pause too much between phrases. My M-q re-wraps paragraphs between one sentence per line, one paragraph per line, and wrapping to fill-column, and it's probably the sort of thing that more voice commands would make even more convenient.

Maybe I can even use that spatial sense of a layout by touching buttons in an Emacs-served webpage on my tablet to direct my dictation to different places. Org-draw uses a similar Emacs web server approach to let you draw on another device and embed the results into an Org file, and there are also simpler HTTP servers (web-server, simple-httpd), so it's probably quite doable. I'm imagining something that takes the top-level items from my current nested list and makes them nice big buttons, so I could have something like "Introduction", "Keyboard", "Reasons", "Experiment", "Hybrid", and "Conclusion", and clicking on those could trigger a POST request to the server inside Emacs which moves my point accordingly. I might even have buttons for various commands. It sounds pretty straightforward to implement.

When my brain comes up with lots of interesting ideas, my org-capture voice commands let me choose which rabbit holes to postpone and which ones I want to dive into.

If I get interrupted by life, I can write on my phone while waiting for the kiddo. Orgzly Revived and Syncthing let me pick up where I left off.

As I get thoughts down, I can use keyboard shortcuts to rearrange things and then convert the list of sentences into paragraphs. I can easily turn text into links using my favourites.

I can imagine combining the audio clips from the text properties with the videos and screenshots to create a quick narrated clip that I can edit (using Emacs, of course), or publishing the narration so that people can listen to it if they like.

This combination of drawing + audio + text works better for me than any of those components alone. Visuals give me anchors for my thoughts. Audio helps with the braindump. Text permits precise, searchable details we can copy. Like George Jones, I find they all have their uses and their strengths.

When I try to dictate longer sections without that kind of visual support, I often lose my train of thought. I want to see where I've been and where I want to go. I don't trust LLMs to clean up that kind of rough transcript; the output doesn't quite resonate with me. I like keeping speech recognition constrained to sentences I can immediately check or reorganize, and I have the recordings as text properties so I can go back to that segment if I need to (at least during the current editing section). That way, I'm never left scratching my head and wondering what I meant.

I love that local speech recognition is getting better and better. I'm curious about whether small language models can do better turn detection to help compensate for my intra-phrase delays when I'm thinking of a word, and whether decision models can automatically classify a sentence into its place in my outline. I think it would be cool if embeddings or other language models could suggest related words or resources as I go along. Lots of interesting possibilities even with the technology that's already available.

Thanks to George Jones for hosting this month's Emacs Carnival on input alternatives. Check out the page for more posts!

View Org source for this post

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

-1:-- Emacs Carnival Oct 2026: Exploring keys and voice input in Emacs (Post Sacha Chua)--L0--C0--2026-10-07T12:18:31.000Z

Sacha Chua: Using Emacs to visualize speaking and target words/phrases in real-ish time

2026-10-07_07-33-03.png
Figure 1: Screenshot showing my list of target phrases and which ones have been recognized

When I'm working with a tutor to improve my French, I sometimes need to push myself to provide more details when I'm sharing and ask more follow-up questions in response to what they're saying, instead of falling back to « D'accord » and « C'est cool. » Visualizing the timeline in real-time lets me see if the turn-taking looks good. I might even need to take a more active role in controlling the conversation if I happen to be meeting a tutor who tends to go on monologues.

When I'm talking out loud on my own for practice, this might also nudge me to reduce the pauses and enrich my vocabulary by practising saying things often enough that they become more automatic.

1. Voice activity detection and speech recognition

This code uses this server vad-file.py, which I generated with Google Gemini. It emits JSONL for voice activity detection events (start/stop) and uses Orukeet to transcribe the speech once the voice activity detection notices it's finished. I also use spacy to look up token information because I want to someday colour the nouns by gender, which might allow me to correct myself in the next sentence instead of hesitating in the previous one.

code for sacha-vad-tutor
;; Report on the balance between my tutor and me
(defvar sacha-vad-tutor-process nil)
(defvar sacha-vad-tutor-dir "~/vendor/orukeet")
(defvar sacha-vad-tutor-command `(,(expand-file-name ".venv/bin/python" sacha-vad-tutor-dir)
                                  "asr.py"
                                  "--spacy-model"
                                  "fr_dep_news_trf"
                                  "-s" "300" "-t"
                                  "VirtualMicSink.monitor=sacha" "alsa_output.usb-AT_AT2040USB_202011110001-00.analog-stereo.monitor=other"))
(defvar sacha-vad-tutor-events nil "List of events as an list of alists (label start stop duration).")
(defvar sacha-vad-tutor-labels '("sacha" "other"))
(defvar sacha-vad-tutor-summary '(("sacha" . 0)
                                  ("other" . 0)))
(defvar sacha-vad-tutor-session-length (* 60 60) "Number of seconds to use for scale.")

(defun sacha-vad-tutor-update-view ()
  "Visualize the last 3 minutes and the overview of the session.
Also keep track of target phrases."
  (interactive)
  (with-current-buffer (get-buffer-create "*tutor stats*")
    (erase-buffer)
    (let* ((total (apply '+ (mapcar 'cdr sacha-vad-tutor-summary)))
           (height 30)
           (width (window-width nil t))
           (max-seconds
            (if sacha-vad-tutor-events
                (apply 'max (mapcar (lambda (o) (alist-get 'end o))
                                    sacha-vad-tutor-events))
              0))
           (threshold 180)              ; last three minutes
           (threshold-seconds (- max-seconds threshold))
           (recent
            (seq-keep
             (lambda (o)
               (when (>= (alist-get 'start o)
                         threshold-seconds)
                 (let-alist o
                   `((label . ,.label)
                     (start . ,(- .start threshold-seconds))
                     (end . ,(- .end threshold-seconds))))))
             (copy-sequence sacha-vad-tutor-events)))
           (recent-total (apply '+ (mapcar (lambda (o)
                                             (-
                                              (alist-get 'end o)
                                              (alist-get 'start o)))
                                           recent))))
      (save-excursion
        (insert
         (format "Last %d minute%s:\n"
                 (/ threshold 60)
                 (if (> (/ threshold 60) 1)
                     "s"
                   ""))
         (mapconcat
          (lambda (o)
            (format "%d%% %s\n"
                    (/ (* 100.0 (cdr o)) recent-total)
                    (car o)))
          (seq-map
           (lambda (group)
             (cons (car group)
                   (apply '+ (mapcar (lambda (o)
                                       (-
                                        (alist-get 'end o)
                                        (alist-get 'start o)))
                                     (cdr group)))))
           (seq-group-by (lambda (o) (alist-get 'label o)) recent)))
         "\n")
        (svg-insert-image
         (sacha-vad-visualize
          sacha-vad-tutor-labels
          recent
          t
          width height))
        (insert
         "\nTotal:\n"

         (mapconcat
          (lambda (o)
            (format "%d%% %s\n"
                    (/ (* 100.0 (cdr o)) total)
                    (car o)))
          sacha-vad-tutor-summary))
        (svg-insert-image
         (sacha-vad-visualize sacha-vad-tutor-labels sacha-vad-tutor-events t
                              width height
                              sacha-vad-tutor-session-length))
        (insert "\n\n"  (sacha-learn-lang-bingo-summary))))))

(defun sacha-vad-handle-end-event (data)
  "Handle the end event."
  (let ((event (list
                (cons 'label (alist-get 'label data))
                (cons 'start (- (alist-get 'timestamp data)
                                (alist-get 'duration data)
                                (or sacha-vad-tutor-start-seconds 0)))
                (cons 'end (- (alist-get 'timestamp data)
                              (or sacha-vad-tutor-start-seconds 0))))))
    (push event sacha-vad-tutor-events)
    (let-alist data
      (incf (alist-get (alist-get 'label data) sacha-vad-tutor-summary 0 nil 'string=)
            .duration))
    (sacha-vad-tutor-update-view)))

(defvar sacha-vad-event-handler-functions '(sacha-vad-speech-process) "Functions to call with the VAD/transcription events.")

(defvar sacha-vad-process-my-speech nil "Non-nil means process my speech through the whisper setup.")
(defun sacha-vad-speech-process (data)
  "Process DATA through my whisper input process if it's my speech.
Add to `sacha-vad-event-handler-functions'."
  (when (and sacha-vad-process-my-speech
             (string= (alist-get 'event data) "final")
             (string= (alist-get 'label data) sacha-vad-my-label))
    (with-current-buffer (get-buffer-create whisper--stdout-buffer-name)
      (erase-buffer)
      (insert (alist-get 'text data))
      (goto-char (point-min)))
    (whisper--handle-transcription-output nil (get-buffer-create whisper--stdout-buffer-name))))

(defun sacha-vad-tutor-filter (proc string)
  "Add and parse."
  (when (buffer-live-p (process-buffer proc))
    ;; split the string by newline?
    (let ((data (with-current-buffer (process-buffer proc)
                  (goto-char (point-max))
                  (save-excursion
                    (insert string))
                  (catch 'waiting
                    (unless (or (eobp) (looking-at "\n*$"))
                      (json-read))))))
      (run-hook-with-args 'sacha-vad-event-handler-functions data)
      (when (string= (alist-get 'event data) "end")
        (sacha-vad-handle-end-event data))
      (when (string= (alist-get 'event data) "final")
        (sacha-vad-handle-text-event data)))))

(defvar sacha-vad-last-speaker nil "Previous speaker.")

(defvar sacha-vad-transcript-buffer nil "If non-nil, add speech to this buffer.")
(defun sacha-vad-add-to-transcript (text)
  "Add TEXT to the transcript buffer."
  (when sacha-vad-transcript-buffer
    (with-current-buffer (get-buffer-create sacha-vad-transcript-buffer)
      (set (make-local-variable 'window-point-insertion-type) t)
      (save-excursion
        (goto-char (point-max))
        (insert text)))))

(defvar sacha-vad-my-label "sacha")
(defun sacha-vad-handle-text-event (data)
  (sacha-vad-add-to-transcript
   (concat
    (if (string= sacha-vad-last-speaker (alist-get 'label data))
        " "
      (concat (if (bobp) "" "\n\n") (alist-get 'label data) ": "))
    (alist-get 'text data)))
  (setq sacha-vad-last-speaker (alist-get 'label data))
  (when (string= (alist-get 'label data) sacha-vad-my-label)
    (sacha-learn-lang-notice-bingo-words (alist-get 'text data))))

(defun sacha-vad-tutor-start ()
  "Start the process."
  (interactive)
  (sacha-vad-tutor-ensure)
  (setq sacha-vad-tutor-summary '(("sacha" . 0)
                                  ("other" . 0))
        sacha-vad-tutor-events nil)
  (with-current-buffer (get-buffer-create "*Transcript*")
    (erase-buffer)
    (display-buffer (current-buffer)))
  (display-buffer (get-buffer-create "*tutor stats*")))

(defun sacha-vad-tutor-ensure ()
  "Start the process if it's not already running."
  (interactive)
  (unless (process-live-p sacha-vad-tutor-process)
    (setq sacha-vad-tutor-process-start-time (current-time))
    (setq sacha-vad-tutor-start-seconds 0)
    (let ((default-directory sacha-vad-tutor-dir))
      (setq sacha-vad-tutor-process
            (make-process
             :name "vad-tutor"
             :command sacha-vad-tutor-command
             :connection-type 'pipe
             :buffer (get-buffer-create "*vad-tutor*")
             :stderr (get-buffer-create "*vad-tutor-err*")
             :filter #'sacha-vad-tutor-filter)))))

(defun sacha-vad-tutor-stop ()
  "Stop the process."
  (interactive)
  (interrupt-process sacha-vad-tutor-process))

;;;###autoload
(defun sacha-js-parse-jsonl-file (file)
  "Parse jsonl as a list"
  (let (results)
    (with-temp-buffer
      (insert-file-contents file)
      (goto-char (point-min))
      (while (not (eobp))
        (condition-case nil
            (push (json-read) results)
          (error (goto-char (point-max))))))
    (nreverse results)))

(defvar sacha-vad-tutor-start-seconds 0 "Number of seconds to consider as the start.")
(defvar sacha-vad-tutor-process-start-time nil "Time that the process started.")

;;;###autoload
(defun sacha-vad-tutor-reset-clock ()
  "Reset the start of the session."
  (interactive)
  (setq sacha-vad-tutor-start-seconds
        (time-to-seconds (time-subtract (current-time)
                                        sacha-vad-tutor-process-start-time)))
  (mapcar (lambda (o) (setcdr o 0)) sacha-vad-tutor-summary)
  (setq sacha-vad-tutor-events nil))

(defun sacha-vad-visualize (labels list &optional output-file width height max-seconds)
  (setq height (or height 25))
  (setq width (or width (window-width nil t)))
  (let* ((svg (svg-create width height))
         (max-seconds (max
                       (or max-seconds 0)
                       (if list
                           (apply 'max (mapcar (lambda (o) (alist-get 'end o))
                                               list))
                         0)))
         (pixels-per-second (/ (* 1.0 width) max-seconds))
         (row-height (/ height (length labels))))
    (dolist (o list)
      (let-alist o
        (let ((y (* row-height (or (seq-position labels .label 'string=) -1))))
          (when (>= y 0)
            (svg-rectangle
             svg
             (* .start pixels-per-second)
             y
             (* (- .end .start) pixels-per-second)
             row-height :fill "green")))))
    (cond
     ((stringp output-file)
      (with-temp-file output-file
        (svg-print svg)))
     (output-file
      svg)
     (t
      (with-current-buffer (get-buffer-create "*vad*")
        (goto-char (point-min))
        (display-buffer (current-buffer))
        (erase-buffer)
        (svg-insert-image svg))))))

(defun sacha-vad-stats (labels list)
  "Return the balance of time talking as (label total percentage)."
  (let* ((max-seconds (apply '+ (mapcar
                                 (lambda (o) (let-alist o (- .end .start)))
                                 list)))
         (durations (mapcar (lambda (o)
                            (cons
                             (car o)
                             (mapcar (lambda (e) (let-alist e (- .end .start)))
                                     (cdr o))))
                          (seq-group-by (lambda (o) (alist-get 'label o))
                                        list))))
    (seq-map
     (lambda (label)
       (let* ((row (cdr (assoc label durations)))
              (total (apply '+ row)))
         `((label . ,label)
           (total . ,total)
           (count . ,(length row))
           (percentage . ,(and total max-seconds (> max-seconds 0) (* 100.0 (/ total max-seconds)))))))
     labels)))

;; (sacha-vad-stats '("sacha" "tutor") (seq-take (sacha-js-parse-jsonl-file "~/proj/speech/output2.log") 5))
;; (sacha-vad-stats '("sacha" "tutor") (sacha-js-parse-jsonl-file "~/proj/speech/output.log"))
;; (sacha-vad-visualize '("sacha" "tutor") (sacha-js-parse-jsonl-file "~/proj/speech/output.log"))
;; (sacha-vad-visualize '("sacha" "tutor") (seq-take (sacha-js-parse-jsonl-file "~/proj/speech/output2.log") 30))

(defun sacha-vad-tutor-group-output (files)
  (let ((grouped-files (seq-group-by
                        (lambda (o)
                          (substring (file-name-base o) 0 10))
                        files)))
    (mapcar
     (lambda (group)
       (let ((start 0))
         (cons (car (cdr group))
               (seq-reduce
                (lambda (prev file)
                  (append prev
                          (let ((max-seconds 0))
                            (prog1 (mapcar (lambda (event)
                                             (incf (alist-get 'start event) start)
                                             (incf (alist-get 'end event) start)
                                             (setq max-seconds (max (alist-get 'end event) max-seconds))
                                             event)
                                           (sacha-js-parse-jsonl-file file))
                              (setq start max-seconds)))))
                (cdr group)
                nil))))
     grouped-files)))

(defun sacha-vad-tutor-make-table (files &optional width height)
  "Make a summary table."
  (let ((labels sacha-vad-tutor-labels)
        (width (or width 500))
        (height (or height 30)))
    ;; combine data as needed
    (append
     '(("Date" "Tutor" "Minutes" "%" "Visualization") hline)
     (seq-map
      (lambda (group)
        (let* ((list (cdr group))
               (base (file-name-base (car group))))
          (if list
              (let-alist (car (sacha-vad-stats labels list))
                (let ((svg-name
                       (expand-file-name
                        (concat (file-name-base (car group)) "-" .label ".svg")
                        "~/proj/french/conversation")))
                  (sacha-vad-visualize
                   (list .label)
                   list
                   svg-name
                   width
                   height)
                  (list
                   (format-time-string "%Y-%m-%d"
                                       (seconds-to-time
                                        (sacha-filename-timestamp (car group))))
                   ;; (org-link-make-string (concat "file:" o) "file")
                   (if (string-match "-conversation-\\([^\\.]+\\)" base)
                       (s-capitalize (substring (match-string 1 base) 0 1))
                     "")

                   (round (/ .total 60))
                   (format "%d%%" .percentage)
                   (org-link-make-string (concat "file:" svg-name)
                                         (concat "file:" svg-name))
                   )))
            (list "?"))))
      (sacha-vad-tutor-group-output files))
     nil)))

2. Noticing bingo phrases

Here's the code that reminds me to use the words or expressions on my to-do list, and then keeps track of how often I've used them, prioritizes the ones that I haven't used yet, and displays these. It can work with either my Whisper dictation workflow or the VAD events server described above.

I keep the list of expressions in file:///home/sacha/proj/french/bingo.txt.

Code to notice when I've used an expression
(defvar sacha-learn-lang-bingo-word-file "~/proj/french/bingo.txt")
(defvar sacha-learn-lang-bingo-words nil "List of words.")
(defvar sacha-learn-lang-bingo-words-hash nil "Data for the counts.")

;;;###autoload
(defun sacha-learn-lang-bingo-words ()
  "Load the list of words from the file."
  (interactive)
  (setq sacha-learn-lang-bingo-words
        (seq-map (lambda (o)
                   (when (= (length o) 1)
                     (setq o (list (car o) 0)))
                   (setq o (append o (list
                                      (string-trim (replace-regexp-in-string
                                                    " *\\[.+?\\] *" " "
                                                    (car o))))))
                   o)
                 (pcsv-parse-file sacha-learn-lang-bingo-word-file)))
  sacha-learn-lang-bingo-words)

;;;###autoload
(defun sacha-learn-lang-bingo-handler (event)
  "Manually register the event."
  (interactive "e")
    (let* ((pos (posn-point (event-end event)))
           (val (get-text-property pos 'bingo)))
      (when val
        (sacha-learn-lang-notice-bingo-words val))))

(defvar-keymap sacha-learn-lang-bingo-map
  "<mouse-1>" #'sacha-learn-lang-bingo-handler)

;;;###autoload
(defun sacha-learn-lang-bingo-summary ()
  "Summarize the words to say."
  (let ((target (seq-filter (lambda (row) (= (elt row 1) 0))
                            sacha-learn-lang-bingo-words))
        (yay (seq-filter (lambda (row) (> (elt row 1) 0))
                         sacha-learn-lang-bingo-words)))
    (concat
     (if target
         (concat "Target: "
                 (mapconcat (lambda (row)
                              (propertize
                               (car row)
                               'keymap sacha-learn-lang-bingo-map
                               'bingo (car row)))
                            target
                            "; ")
                 "\n\n")
       "")
     (if yay
         (concat
          "Yay! "
          (mapconcat (lambda (row)
                       (propertize (format "%s (%s)" (car row) (elt row 1))
                                   'keymap sacha-learn-lang-bingo-map
                                   'bingo (car row)))
                     (seq-filter (lambda (row) (> (elt row 1) 0))
                                 sacha-learn-lang-bingo-words)
                     "; ")
          "\n\n")
       ""))))

;;;###autoload
(defun sacha-learn-lang-notice-bingo-words (text)
  "Look for the bingo words in TEXT and celebrate when I use them.
Add to `whisper-insert-text-at-point'."
  (when text
    (unless sacha-learn-lang-bingo-words (sacha-learn-lang-bingo-words))
    (let ((found (seq-find
                  (lambda (o)
                    (and (elt o 2)
                         (string-match (concat "\\<" (elt o 2)) text)))
                  sacha-learn-lang-bingo-words)))
      (when found
        (incf (elt found 1))
        (sacha-vad-tutor-update-view)
        (with-current-buffer (get-buffer-create "*Bingo*")
          (erase-buffer)
          (insert (sacha-learn-lang-bingo-summary))))))
  text)
This is part of my EmacsConfiguration.
View Org source for this post

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

-1:-- Using Emacs to visualize speaking and target words/phrases in real-ish time (Post Sacha Chua)--L0--C0--2026-10-07T11:47:04.000Z

Protesilaos: Emacs: spontaneous live new ‘denote-data’ feature (2026-10-07 at 18:00 Europe/Athens)

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

[ The stream will be recorded. You can watch it later. ]

This is a spontaneous live stream. I am working on a new feature for my ‘denote’ Emacs package. The idea is to implement an optional in-memory cache that can help speed up certain operations.

-1:-- Emacs: spontaneous live new ‘denote-data’ feature (2026-10-07 at 18:00 Europe/Athens) (Post Protesilaos)--L0--C0--2026-10-07T00:00:00.000Z

Irreal: The Power Of Plain Text

Over at the Dead Parrot BBS, nickb has an excellent post extolling the power of plain text. It’s a notion that’s familiar to all us Emacs users and even more so to Org users. We Org users (or more generally Markdown and other markup users) can write plain text and have it formatted for output in a variety of ways. The point is, the text stays text and is readable even without the formatting.

As nickb says, text written today will still be readable 50 years from now. Sure, UTF-8 and the other Unicode formats can and do add new characters from time to time but text written today will have the same meaning even if new characters are added.

The most important aspect is that you’ll own your data no matter what programs come and go. Emacs isn’t going anywhere but even if it did, it wouldn’t matter. Text will still be readable and something like Org or Markdown will still make intended formatting obvious even if Org and Markdown no longer exist.

Nickb acknowledges that text is not the right answer for everything. You wouldn’t, for example, want to represent a photograph in plain text and there are lots of other examples. Still, for many applications, plain text is not only sufficient but probably the better solution.

-1:-- The Power Of Plain Text (Post Irreal)--L0--C0--2026-10-06T17:53:04.000Z

James Cherti: How to make the Emacs UI feel more responsive

When I first changed from Vim to Emacs a few years ago, the editor's user interface felt sluggish. I quickly learned that the Emacs UI itself is not slow. Its default settings can make it feel that way. To restore the instant feedback loop I was used to, I needed to enhance Emacs defaults. (FYI: Enhancing default settings is what drove me to develop minimal-emacs.d) The configurations below are the result of my tuning process to enhance the perceived Emacs UI responsiveness and latency. These settings are best treated as independent experiments. I recommend testing these adjustments iteratively. Before we … Continue reading →
-1:-- How to make the Emacs UI feel more responsive (Post James Cherti)--L0--C0--2026-10-06T15:38:52.000Z

Sacha Chua: Emacs Lisp: Parse page ranges, insert issue references

David Pape asked:

I just typed

>Closes: #47, #48, #49, #50, #51, #52, #53, #54

into a commit description, all by hand.

Is there a way in Emacs to expand some kind of range description to make this easier? 😅

I want to expand strings like "5-10, 15-18, 3" into numbers.

Hmm, could be useful. It reminds me of how page ranges work, which will be handy if I want to tinker with PDFs. Let's see if something like this works:

(defun sacha-parse-page-range (s)
  "Parse a page range string and return a list of numbers.
Ex: 1-5
2-4, 6-10
1 4 7-9
3-11/2   - this one specifies an increment"
  (seq-mapcat
   (lambda (part)
     (let ((nums (mapcar 'string-to-number (split-string part " *[-/] *"))))
       (if (= (length nums) 1)
           nums
         (number-sequence (elt nums 0) (elt nums 1) (elt nums 2)))))
   (split-string s "[, \t\n\r]+")))

(ert-deftest sacha-parse-page-range ()
  (should (equal (sacha-parse-page-range "5-10")
                 '(5 6 7 8 9 10)))
  (should (equal (sacha-parse-page-range "1-5, 8")
                 '(1 2 3 4 5 8)))
  (should (equal (sacha-parse-page-range "1 4 7-9")
                 '(1 4 7 8 9)))
  (should (equal (sacha-parse-page-range "3-11/2")
                 '(3 5 7 9 11))))

(defun sacha-issue-range (range)
  "Expand RANGE to a list of issues and insert them."
  (interactive "MRange (ex: 1-5, 7): ")
  (let ((s (mapconcat (lambda (n) (format "#%d" n))
                      (sacha-parse-page-range range)
                      ", ")))
    (when (called-interactively-p 'any)
      (insert s))
    s))

See also:

This is part of my EmacsConfiguration.
View Org source for this post

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

-1:-- Emacs Lisp: Parse page ranges, insert issue references (Post Sacha Chua)--L0--C0--2026-10-05T21:30:38.000Z

Marcin Borkowski: Emacs Lock mode

Today I only have a short tip. Sometimes you may have an important Emacs buffer you don’t want to kill accidentally. (For example, it might be an Eshell buffer with a long-running process, or a scratch buffer not associated with any file where you have some notes – although that is obviously asking for trouble.)
-1:-- Emacs Lock mode (Post Marcin Borkowski)--L0--C0--2026-10-05T19:26:14.000Z

Charles Choi: Announcing triode.el

A long-standing passion of mine is listening to and collecting music. I’m always on the lookout for new music and where I discover it is primarily from internet radio stations. That said, listening to music in such a way is largely a passive activity, where your main focus is somewhere else. The better music players that acknowledge this difference in focus provide mechanisms for remote-controlling them from other apps. So it is with the Triode app for macOS, which leverages Cocoa frameworks (App Intents in particular) to support remote control.

triode.el is an Emacs Lisp library which provides a simple remote control interface to the Triode app. Basic support for selecting a radio station, play/stop, and muting are provided. Not every feature in the Triode app is accessible to triode.el though; this is controlled by the App Intents actually implemented by its developer, The Iconfactory.

img

Interested readers can learn more about triode.el in its user guide.

triode.el is now available on MELPA. This release joins my other packages that support remote control of music apps on macOS from Emacs:

All the above efforts have been an exploration of malleable computing using Emacs as the main orchestration tool. I plan on posting a more technical deep dive reflecting this work in the not too distant future. That said, if you’re a macOS user who uses Emacs and Triode, I encourage you to give triode.el a try. Also, consider this a plug for Triode app, a well-designed app that does a great job of taking advantage of the Cocoa frameworks to provide internet radio across different devices provided by Apple.

-1:-- Announcing triode.el (Post Charles Choi)--L0--C0--2026-10-05T17:50:00.000Z

Irreal: The Future Of Emacs

Chris Maiorana asks a provocative question: What’s the future of Emacs? He asks that question in the context of assuming that Emacs will continue to exist for any timeline that matters to us. But how, he asks, will we use it?

The answer that will occur to most of us is speech mediated input á la Star Trek TNG. We’ll simply tell Emacs what we want it to do and it will do it. But think about that for a moment. Would you really want to dictate the code for your latest program to the computer? Or would you even want to dictate your documentation, story, or letter to your computer?

Sure, you could say something like, “Computer write a program to do such and such.” but then you’re not using Emacs at all. The same for asking the computer to write something for you. That—more or less—is here today but most of us don’t like the results and, again, we’re not really using Emacs in those situations.

For my part, my written thoughts are always much better organized than my spoken ones so even with capable voice input, I’d much prefer to use the keyboard. Maiorana agrees. He points back to a couple of episodes from Tales From the Darkside1 in which people use the keyboard to do magic with the computer. For him, Meta+x is the spell that enables the magic and he’s very attached to it. Like me, he doesn’t anticipate abandoning the keyboard.

What do you think? Will we still use the keyboard or will we move on to something better?

Footnotes:

1

You can find Mookie and Pookie and Word Processor of the Gods in the Internet Archive.

-1:-- The Future Of Emacs (Post Irreal)--L0--C0--2026-10-05T15:11:13.000Z

Sacha Chua: 2026-10-05 Emacs news

The Emacs Carnival for October is about looking into your crystal ball. I'd love to see what you're looking forward to exploring. Thanks to George Jones for hosting!

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

View Org source for this post

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

-1:-- 2026-10-05 Emacs news (Post Sacha Chua)--L0--C0--2026-10-05T14:01:16.000Z

Protesilaos: Emacs: ‘doric-meadow’ and ‘doric-mountain’ coming to the ‘doric-themes’

Version 1.4.0 of the doric-themes, which is currently in development, will include two new themes that draw inspiration from natural settings.

The Doric project is all about restraining the use of colour and being deliberate about the application of typographic styles. The result is a minimalist aesthetic that can make it easier to focus on the contents of the Emacs buffer.

Below are samples of the two themes. For accurate results, click to enlarge the image.

doric-meadow

doric-meadow theme sample

doric-meadow theme sample

doric-meadow theme sample

doric-meadow theme sample

doric-mountain

doric-mountain theme sample

doric-mountain theme sample

doric-mountain theme sample

doric-mountain theme sample

Sources

The Doric themes use few colours and will appear monochromatic in many contexts. They are my most minimalist themes. Styles involve the careful use of typographic features and subtleties in colour gradients to establish a consistent rhythm.

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

-1:-- Emacs: ‘doric-meadow’ and ‘doric-mountain’ coming to the ‘doric-themes’ (Post Protesilaos)--L0--C0--2026-10-05T00:00:00.000Z

Ashish Panigrahi: This website is now built with Org mode

Since moving to Zola as my static site generator, I have had a nice setup for my website. However, a recent update to zola (v0.22 -> v0.23) broke my website (it apparently had something to do with the Tera templating engine that zola uses). This forced me to finally make the move to switching to orgmode as my SSG.

My ultimate goal has always been to build this website purely with emacs and orgmode[1], and I'm glad now that's the case. Full disclosure however, I did take the help of LLMs to implement this over the weekend. There are still some rough edges to the site, which is why I have this build on a different branch (instead of main) on my website's source repository.

The website is generated via org-publish with the following directory structure for the source code:

.
├── LICENSE
├── Makefile
├── README.org
├── content/
├── lisp/
├── publish.el
├── static/
├── statichost.yml
├── style.css
└── templates/

5 directories, 6 files

The key component is publish.el which exports all of my blogposts (which are orgmode files now) to html files. I also have the htmlize.el file hosted on the git repo for syntax highlighting.

Feel free to peruse the source code of this build if you're interested in the technical details. At its current state, I'm quite happy with how it has turned out. A nice little addition (which is the default behaviour of org-publish) is that I now have a footer entry mentioning the emacs and orgmode version used to build this website!

  1. As is the trend to move all of my computing needs to emacs. ↩

-1:-- This website is now built with Org mode (Post Ashish Panigrahi)--L0--C0--2026-10-05T00:00:00.000Z

Chris Maiorana: M-x and the future of Emacs

How will the future affect Emacs? How will it affect the keyboard itself? Will we speak commands to be interpreted by language models? Will we direct a swarm of bots to do our bidding like an over-zealous Howard Hughes with a megaphone? Or, are keyboards and M-x here to stay?

I’ve been thinking back a lot recently on a few of my favorite episodes of Tales From the Darkside, a horror anthology series that ran from 1983 to 1988, a recurring favorite on the Sci-fi channel, later collected on DVD, and probably streaming somewhere today. Two episodes from the first season are particularly memorable for me: “Mookie and Pookie” and “Word Processor of the Gods.” If you’re an Emacs user and haven’t seen these two great episodes of television, you have some homework to do. Google up, or do whatever it is you do when you need to search for something these days, and find these. At the very least, you will recognize the source of the background image I’ve used in countless YouTube video thumbails.

In “Mookie and Pookie” (original air date 11 November 1984) a terminally (pun not lost on anyone) ill young man uploads his consciousness into a computer, leaving behind a special set of printed instructions for his sister, Pookie (Justine Bateman), on how to invoke his spirit in “the network” after he is gone.

Hold that thought in memory for just a moment.

In “Word Processor of the Gods,” (original air date 25 November 1984) based on a short story by Stephen King, a man receives a homebrew computer as a birthday gift from his recently deceased nephew. This special computer gives the user the ability to create, edit, and delete aspects of his own life—basically the power to author reality.

Create, read, update, and delete your own life in “Word Processor of the Gods.”

Is any of this reminding you of Emacs yet?

These tales represent the hope and anxiety that people had about computers in the 1980s. Those magical boxes were either angels bringing benefits to mankind or devils that would lead to our doom. With the benefit of hindsight, we can see that computers brought a mixed bag of benefits and drawbacks, but no one wants to go back to a time before we had them.

I feel quite privileged that I got to see the birth of the first person shooter, the Internet, the smartphone, and “AI” (jury still being out on whether or not language models are really “artificial intelligence”), and have lived long enough to tell about it.

But we are drifting here from the thrust of this article, which is basically to pontificate on the question of “How will the future affect Emacs?”

Let’s go back to “Mookie and Pookie” and the image of teenage Justine Bateman typing out programs by hand, on a keyboard, in that episode of Tales From the Darkside. At the start of the episode, the girl who hates computers is nervously typing one command after another, trying to get a feel for this mysterious box. But soon, she’s become a hacker, accessing data banks and interfacing with the soul of her dead brother. We can see she’s not just typing commands but learning magic.

Justine Bateman typing out programs in “Mookie and Pookie.”

Nothing so closely matches the experience I had learning Emacs. Except, of course, that I was a thirty-year-old man, not a teenage girl.

Nevertheless, “Mookie,” like “Word Processor of The Gods,” is about using the keyboard as a conveyance for invoking magic.

Fast forward to my discovery of Emacs. Using the help system was like opening a book of magic spells. The keyboard was my wand, artist’s brush, sculptor’s trowel, chef’s saute pan, or whatever artistic/magical metaphor you prefer. This magic is achieved by invoking M-x.

Invoking M-x is the most sensuous and gratifying experience in all of computing.

That simple key command opens a whole world of possibilities.

There’s nothing quite like it. Even though I can set a custom key-binding for a frequently used function, I often still prefer not, because M-x is my silver key.

But this is how we use Emacs today. Now, let’s get to the brave new world.

Computing is about choice, especially in our open source communities. That will never change. I don’t know how future developments will affect Emacs, no one does. But I don’t see how it could change much for those who choose to use it over newer and flashier options.

It’s easy to see how a coding agent could nullify a text editor for a segment of users, but that would be applicable to all text editors—not just Emacs.

As any Emacs user can attest, it’s more than a text editor. Learning the ways of Emacs is learning the ways of Lisp which is learning about the nature of computation itself. That journey is worth taking for its own sake. No “agentic” expedient can replace it.

People are still learning and speaking “dead” languages like Latin and Ancient Greek. Because they’re not dead at all. They’re just as vibrant as ever in the places they are spoken (mostly in churches and legal offices) and in the languages that have been built from them.

While I do my best to welcome and understand any new technology, and reserve the right to tinker and test and taste to my heart’s content, I have no immediate plans on giving up my precious M-x. Not for nothing.

The post M-x and the future of Emacs appeared first on Chris Maiorana.

-1:-- M-x and the future of Emacs (Post Chris Maiorana)--L0--C0--2026-10-04T14:59:02.000Z

Irreal: Nested Emacs Frames

Just a quick offering today on an informative post from Ankit Gadiya about unwanted nested frames in Emacs. The TL;DR is that Gadiya runs Ghostel as his Emacs shell in TUI and demon mode. That works well but he has a script that calls emacsclient to load a file. There’s nothing special about it; it’s just what you’d expect it to be but when he runs it from with Ghostel, it creates a nested frame in the Ghostel frame rather than replacing the Ghostel frame.

The solution is pretty simple. Gadiya changed his script to check if he was in Ghostel, and if so, calls emacsclient with a parameter to reuse the current frame. You can check Gadiya’s post for the details.

For me, the most interesting aspect was how he checked if he was in Ghostel. It turns out that Emacs sets the environment variable INSIDE_EMACS when it’s running a shell or subprocess. I didn’t know that so I learned something new from Gadiya’s post.

Update [2026-10-05 Mon 11:15]: Added link to Gadiya’s post.

-1:-- Nested Emacs Frames (Post Irreal)--L0--C0--2026-10-04T14:58:04.000Z

Ankit Gadiya: Fix nested Emacs frames

Earlier this year I added Ghostel to my Emacs configuration. Ghostel is a terminal emulator for Emacs that uses Ghostty's engine underneath. It is a delight to use especially with the TUI applications.

Nested Emacs frame problem

I use Emacs in daemon mode, and then use emacsclient to open new frames from the daemon. To use the same Emacs daemon when opening files through other programs such as git, sops, I have a small shell script ee configured as my default EDITOR. It opens a new Emacs frame in the terminal.

#!/bin/sh

exec emacsclient --create-frame --no-window-system "$@"

When I run this script from Ghostel, it opens a nested Emacs frame in Ghostel's terminal session.

Nested Emacs Frame

Solution

Emacs sets an environment variable INSIDE_EMACS for shells and subprocesses. The script use this variable to check if it is launched from Ghostel and use the --reuse-frame flag to open the new buffer in the current frame.

#!/bin/sh

if [ "$INSIDE_EMACS" = "ghostel" ]
then
  exec emacsclient --reuse-frame "$@"
fi

exec emacsclient --create-frame --no-window-system "$@"
-1:-- Fix nested Emacs frames (Post Ankit Gadiya)--L0--C0--2026-10-03T00:00:00.000Z

Raymond Zeitler: What is spook?

What is spook?

The Emacs function spook is not what you think it is. It doesn't make creepy noises, draw scary ASCII art or change the theme to orange on black. In fact it has nothing at all to do with Halloween, ghosts, monsters, pandemics or IRS audits.

What it does is insert words and phrases into the current buffer "to help give your message ... attractiveness for automated keyword scanners."

What kind of words or phrases might you expect to see? To find out I invoked spook while composing this content. Here's what it added:

TELINT Drug North Korea Security Consulting Virii Abu Ghraib Fort Hancock CUD AMEMB Vaccine MARTA racal E.O.D. Blizzard Nigeria

It's easy1 to configure spook to insert an alternate set of words and phrases -- just change the file name in the variable spook-phrases-file from "spook.lines" to the name of the alternate file. Unfortunately it must reside in the Emacs data directory; the file's location is fixed. The variable can be found in the Spook customization group, whose parent group is Games.

Even with only 72 lines in its source code file,2 spook demonstrates how to insert one or more random lines from a file into the current buffer.


1 It's "easy" if you have Administrator permissions.

2 The 72 lines is the entire spook.el file; the first 48% of the file provides the usual comments that describe the GNU GPL, author, maintainer info as well as documentation.

-1:-- What is spook? (Post Raymond Zeitler)--L0--C0--2026-10-02T02:17:46.693Z

Protesilaos: About Omarchy and reasonableness

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

This is a summary of comments I made at the ‘Root Access’ podcast. They are on the topic of whether to use Omarchy or not on the basis of how one feels about its author.

I personally prefer the complete episode because clips can distort the vide of the event, such as by making me look angry when in fact we are laughing throughout: https://protesilaos.com/codelog/2026-09-27-emacs-live-root-access-show/.

About ‘Root Access’

Root Access is a live conversation with Linkarzu, CEO members, and invited guests about tech, tools, workflows, and what they’re building. Guests can share their projects before we dive into the discussion. Everyone is welcome to watch live and join the chat; the full replay stays public.

Support @linkarzu

-1:-- About Omarchy and reasonableness (Post Protesilaos)--L0--C0--2026-10-02T00:00:00.000Z

Protesilaos: About Emacs, AI contributions, and the politics of copyright

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

This is a summary of comments I made at the ‘Root Access’ podcast. They are on the topic of AI-generated contributions to Emacs and the wider discussion on free software politics.

I personally prefer the complete episode because clips can distort the vide of the event, such as by making me look angry when in fact we are laughing throughout: https://protesilaos.com/codelog/2026-09-27-emacs-live-root-access-show/.

About ‘Root Access’

Root Access is a live conversation with Linkarzu, CEO members, and invited guests about tech, tools, workflows, and what they’re building. Guests can share their projects before we dive into the discussion. Everyone is welcome to watch live and join the chat; the full replay stays public.

Support @linkarzu

-1:-- About Emacs, AI contributions, and the politics of copyright (Post Protesilaos)--L0--C0--2026-10-02T00:00:00.000Z

Please note that planet.emacslife.com aggregates blogs, and blog authors might mention or link to nonfree things. To add a feed to this page, please e-mail the RSS or ATOM feed URL to sacha@sachachua.com . Thank you!