I want to read articles from my RSS subscriptions on my phone without signing
up to a hosted service or running a public server.
Elfeed, the Emacs feed reader, already lets me manage my subscriptions and read
from within Emacs. It comes with an experimental web UI, but needs the server
(running on my laptop) to be accessible whenever I want to read. My phone
becomes useless the moment my laptop sleeps.
So, I built an alternate web UI with a service worker that caches content on
the client side for a smooth offline reading experience. It’s written in OCaml
and compiled to JavaScript using js_of_ocaml.
Why bother with RSS feeds?
It is annoyingly easy to fall into the trap of short form videos or other
algorithmically “curated” feeds and get sucked into scrolling mindlessly. I
fear this is going to get even worse with all the generative AI stuff.
I want to be more deliberate about what I consume. RSS feels calmer and gives
me a greater sense of control. Or it may just be nostalgia from good old Google
Reader days.
Why Emacs and Elfeed?
Since Google Reader was killed, I’ve bounced between many readers but none of
them has really stuck with me. I’ve had different phases of using Elfeed, over
the years, though.
I like the fact that everything is local with Elfeed – no hosted services, no
public servers. And since it lives inside Emacs, everything from tagging to how
entries are displayed can be tweaked.
Why Elfeed offline?
Elfeed also ships a basic web UI that lets me read my feeds from a different
device, but only when my laptop is online and reachable. I’d like to be able to
read these posts even when I’m “on the road”, say, while waiting for a train or
while taking a cab ride.
I knew it should be possible to do this with some client side caching. And, I
considered improving the web UI of Elfeed itself but it doesn’t seem to be
actively maintained in the last couple of years. Creating a separate project
would also give me more freedom to experiment.
How does it work?
Elfeed offline comes with a tiny Dream based webserver that acts as a proxy in
front of the Emacs Elfeed webserver for the API requests. The Dream web server
also serves the static assets like the HTML, JavaScript, stylesheets and the
Service Worker for the web app. The diagram below shows how data flows between the different components.
The Service Worker intercepts all the GET requests from the client and
responds using a cache-first-with-cache-refresh strategy. This makes the
reading experience feel very responsive. The web client also implements a “good
enough” clone of the original search functionality of Elfeed to allow searching
and filtering content while offline.
The web app also lets me mark entries as read or star them when I’m offline.
The Service Worker caches these operations and updates the server when the
server becomes reachable. There’s no smart conflict resolution here, and the
API requests just overwrite and update the state on the server.
I have been using this for a couple of weeks now, and am quite happy with how
responsive the UI feels and the number of posts I managed to read so far.
But, there are definitely some rough edges to smoothen out.
It’s a pain to make sure that both the servers are running when I’m online
and want to sync new updates to my phone. I’m considering adding a tiny Emacs
helper to manage this.
I need to remember to open the app on my phone to trigger a cache update
before stepping away from my laptop. I want to explore using the Background
Sync API (not available on Firefox) to possibly make this easier.
The UI could use some improvements to indicate pending syncs when offline and
also indicate the last sync with the server to help ensure everything is
synced before stepping away from my laptop.
Service workers need the server to be accessible via https. I have a wrapper
script around mkcert to make this easy, but some apps on my phone refuse to
work correctly when there are user installed certificates.
Why OCaml?
At work, I write OCaml every day and enjoy it. This project seemed like a good
one to try out js_of_ocaml since everything is local and I don’t need to
worry about bundle sizes, etc.
js_of_ocaml makes it quite convenient to share code between the server and
the client side. And given there’s also a “third” Service Worker component in
the mix, I thought it would make life easier. For instance, the message types
and the serialization code for the messages between the client and the service
worker are shared.
I’m also using this project to experiment with Dune’s “soon to be released”™
package management that our team at Tarides is working on building.
How can you use it?
You can try out Elfeed offline’s UI here as a static site (with some posts from
Planet Emacslife and The OCaml Planet). You should be able to read posts,
search and filter for posts, mark them as read or star them. Any changes you
make should be preserved between reloads.
If you are an Emacs user but don’t use Elfeed, you can checkout the Elfeed’s
README for instructions to set it up. It also has links to a bunch of blog
posts and videos that show off the features of Elfeed.
If you are already using Elfeed, you should be able to set it up quite easily
if you’re happy installing some OCaml tooling. The installations instructions
are available in the README.
Outro
I have been enjoying using Elfeed offline for the past couple of weeks. And I’m
hoping I’ll end up spending much more time reading in it, than fixing and
building it. It currently definitely feels like something I built for myself,
but I’d love to hear from others who try it out.
Weaning myself from Emacs is like a minor hobby for me. Or at least you’d think it was, based on how much time I spend on it. I’ve only ever succeeded once or twice, and only for a short time. There is simply nothing like Emacs and definitely nothing like Org mode. Besides, I have a decade of notes in there. Nearly every note-taking, text-editing problem I’ve ever run into has been solved either by me or someone else in Emacs. I’m comfy there.
And yet, last week I tried leaving Emacs again.
I was sucked in by the promise of Markdown being available and useful just about anywhere, with any modern tool. I was tired of C-x C-something for everything. For example, opening my Emacs bookmarks means C-x r b and for some reason I always have to look at the keyboard while typing it. Yes, yes, I can rebind it if it bugs me, but that’s a can of worms I’ve regretted opening before.
It’s possible that the thing I thought about most has been the native Emacs bindings in nearly every app on macOS and the fact that they’re not everywhere on Linux. Assuming I’ll never be able to go all in with only macOS or Linux, I thought I might try covering more bases by switching to Evil mode in Emacs. True Vim bindings aren’t always available, but most apps and CLI tools, etc can fake it pretty well. Vim can fake it pretty well, too, and I spend a lot of time there.
So I dug out my vanilla evil-mode config and went to town. I hated it immediately. Mine was a half-ass configuration I cobbled together from a bunch of blog/Reddit posts. It was inconsistent and broken in places.
You know what does Evil mode really well? Doom Emacs.
So I spent a few hours this morning (re)installing Doom and migrating the important bits of my vanilla config over.
Doom tries to do too much, but it does a pretty good job of it. I’m modal-editing my way around everything and it’s like coming home. I’m sure I’ll start stubbing my toes on things at some point, but for now, the problems I traded for it are worth it.
Toggle help with the "?" key add cities with the "+" key. Shifting time is possible via the "f" / "b" keys, in addition to a other features available via the "?" help menu.
Hope you enjoyed the video!
Want more videos?
Liked the video? Please let me know. Got feedback? Leave me some comments.
Looking for something to write about? Christian Tietze is hosting the January Emacs Carnival on the theme "This Year, I'll…". Check out last month's carnival on The People of Emacs for other entries.
Like many Emacsers, I am a heavy Magit user. If you use Magit, I don’t need to tell you how great it is; if you don’t, I suggest you do yourself a favor and try it out. That doesn’t mean that Magit is ideal, though. It has some issues, though usually very minor ones. Today I’d like to write about something which is definitely not a “Magit issue”, but rather something I personally miss in it.
I’m a long term user of Dumb Jump. According to this Irreal post, I’ve been a devoted fan since 2017. As I’ve said many times in my several posts about Dumb Jump—search for “dumb jump” on Irreal if you’re interested—I’ve never been able to warm up to TAGS systems because they require so much maintenance and LSP systems have always seemed like too much work for what I want them for.
It turns out that I’m not the only fan. Ruslan Bekenev has a lovely paean to Dumb Jump. He, like me, says it has completely eliminated his need for TAGS or LSP. For all the things that I use it for it’s instantaneous. That can be difficult to believe when you learn how it works but it’s true. You can get the details from Bekenev’s post or the Dumb Jump site but the TL;DR is that it uses grep to search for the desired target. I use ripgrep with it so it’s fast with even big repositories. Bekenev has an animated GIF showing how it works for him.
If you haven’t tried Dumb Jump, I join with Bekenev in urging you to try it out. It’s small, doesn’t require a complicated installation and is easy to configure. If you don’t like it, you can simply delete the package and remove it from your init.el.
Welcome to 2026, and a new year of Emacs Blog Carnival post!
To start the Gregorian calendar year fresh, the blogging/writing/thinking prompt of this month is:
This year, I’ll …
What will you do differently in Emacs this year? Or will you just get started? Why?
What are you excited about to explore and tinker with? What do you want to perfect?
Are there any large projects in fron of you that you’ll emacs1 with confidence?
What’s a Blog Carnival, Again?
A blog carnival is a fun way to tie together a community with shared writing prompts, and marvel at all the creative interpretations of the topic of the month. I’ve provided a couple of interpretations above, but you may think of something else entirely. That’s amazing, roll with it, that’s what makes this fun!
For future Carnivals, check out the “Carnival” page on EmacsWiki . It includes instructions, and is our community space to coordinate participants and topic ideas.
Submissions
Comment below or DM/email me with your submission! I’ll collect submissions up to, and including, February 1st (Central European Time), so that every time zone has had a chance to meet the January 31st deadline.
Substitute provides a set of commands that perform text replacement
(i) throughout the buffer, (ii) limited to the current definition (per
narrow-to-defun), (iii) from point to the end of the buffer, and
(iv) from point to the beginning of the buffer. Variations of these
scopes are also available.
These substitutions are meant to be as quick as possible and, as such,
differ from the standard query-replace (which I still use when
necessary). The provided commands prompt for substitute text and
perform the substitution outright, without moving the point. The
target is the symbol/word at point or the text corresponding to the
currently marked region. All matches in the given scope are
highlighted by default.
Backronym: Substitutions Uniformly Beget Standardisation for Text Invariably Transfigured Unto This Entry.
Below are the release notes.
Version 0.5.0 on 2026-01-05
This is a small release that fixes a bug and adds a relevant user
option.
The bug pertained to the scope of the substitution when buffer
narrowing was in effect. All commands would ignore narrowing and also
fail to properly clear the highlights they apply (highlights are
transiently in effect to show what the target of the substitution is
while the minibuffer is waiting for user input).
Now all commands do the right thing with respect to buffer narrowing.
Though their exact behaviour depends on the value of the new user
option substitute-ignore-narrowing:
When the value of substitute-ignore-narrowing is non-nil, then
substitutions apply to the actual scope of the given command. For
example, substitute-target-in-buffer will cover the whole buffer
from the absolute minimum position to the absolute maximum position
even if narrowing is in effect.
When the value of substitute-ignore-narrowing is nil, then
substitutions apply to their scope subject to the boundaries of the
narrowed buffer. For example, substitute-target-in-buffer will
understand as “whole buffer” the region between the minimum and
maximum positions of the narrowed buffer.
Users can write small convenience commands that do either of those,
depending on preference. For example:
(defunmy-substitute-target-in-buffer-with-narrowing()"Call `substitute-target-in-buffer' with `substitute-ignore-narrowing' as `nil'."(let((substitute-ignore-narrowingnil))(call-interactively'substitute-target-in-buffer)))(defunmy-substitute-target-in-buffer-without-narrowing()"Call `substitute-target-in-buffer' with `substitute-ignore-narrowing' as non-`nil'."(let((substitute-ignore-narrowingt))(call-interactively'substitute-target-in-buffer)))
: Added note about difference from MELPA package, fixed :vc
I want to get my thoughts into the computer quickly, and talking might be a good way to do some of that. OpenAI Whisper is reasonably good at recognizing my speech now and whisper.el gives me a convenient way to call whisper.cpp from Emacs with a single keybinding. (Note: This is not the same whisper package as the one on MELPA.) Here is how I have it set up for reasonable performance on my Lenovo P52 with just the CPU, no GPU.
I've bound <f9> to the command whisper-run. I press <f9> to start recording, talk, and then press <f9> to stop recording. By default, it inserts the text into the buffer at the current point. I've set whisper-return-cursor-to-start to nil so that I can keep going.
(use-package whisper
:vc (:url"https://github.com/natrys/whisper.el")
:load-path"~/vendor/whisper.el":config
(setq whisper-quantize "q4_0")
(setq whisper-install-directory "~/vendor")
;; Get it running with whisper-server-mode set to nil first before you switch to 'local.;; If you change models,;; (whisper-install-whispercpp (whisper--check-install-and-run nil "whisper-start"))
(setq whisper-server-mode 'local)
(setq whisper-model "base")
(setq whisper-return-cursor-to-start nil)
(setq whisper--ffmpeg-input-device "alsa_input.usb-Blue_Microphones_Yeti_Stereo_Microphone_REV8-00.analog-stereo")
;(setq whisper--ffmpeg-input-device "VirtualMicSink.monitor")
(setq whisper--ffmpeg-input-device "audiorelay-virtual-mic-sink:monitor_FL")
(setq whisper-language "en")
(setq whisper-before-transcription-hook nil)
(setq whisper-use-threads (1- (num-processors)))
(setq whisper-transcription-buffer-name-function 'whisper--simple-transcription-buffer-name)
(add-hook 'whisper-after-transcription-hook'my-subed-fix-common-errors-from-start)
:bind
(("<f9>" . whisper-run)
("C-<f9>" . my-whisper-org-capture-to-clock)
("S-<f9>" . my-whisper-replay)
("M-<f9>" . my-whisper-toggle-language)))
The technology isn't quite there yet to do real-time audio transcription so that I can see what it understands while I'm saying things, but that might be distracting anyway. If I do it in short segments, it might still be okay. I can replay the most recently recorded snippet in case it's missed something and I've forgotten what I just said.
(defunmy-whisper-replay ()
"Replay the last temporary recording."
(interactive)
(mpv-play whisper--temp-file))
Il peut aussi comprendre le français.
(defunmy-whisper-toggle-language ()
"Set the language explicitly, since sometimes auto doesn't figure out the right one."
(interactive)
(setq whisper-language (if (string= whisper-language "en") "fr""en"))
;; If using a server, we need to restart for the language
(when (process-live-p whisper--server-process) (kill-process whisper--server-process))
(message "%s" whisper-language))
I could use this with org-capture, but that's a lot of keystrokes. My shortcut for org-capture is C-c r. I need to press at least one key to set the template, <f9> to start recording, <f9> to stop recording, and C-c C-c to save it. I want to be able to capture notes to my currently clocked in task without having an Org capture buffer interrupt my display.
To clock in, I can use C-c C-x i or my !speed command. Bonus: the modeline displays the current task to keep me on track, and I can use org-clock-goto (which I've bound to C-c j) to jump to it.
Then, when I'm looking at something else and I want to record a note, I can press <f9> to start the recording, and then C-<f9> to save it to my currently clocked task along with a link to whatever I'm looking at.
(defvarmy-whisper-org-target nil
"*Where to save the target.Nil means jump to the current clocked-in entry and insert it along witha link, or prompt for a capture template if nothing is clocked in.If this is set to a string, it should specify a key from`org-capture-templates'. The text will be in %i, and you can use %a for the link.For example, you could have a template entry like this:\(\"c\" \"Contents to current clocked task\" plain (clock) \"%i%?\n%a\" :empty-lines 1)If this is set to a function, the function will be called from theoriginal marker with the text as the argument. Note that the windowconfiguration and message will not be preserved after this function isrun, so if you want to change the window configuration or display amessage, add a timer.")
(defunmy-whisper-org-capture-to-clock ()
(interactive)
(require'whisper)
(add-hook 'whisper-after-transcription-hook#'my-whisper-org-save 50)
(whisper-run))
(defunmy-whisper-org-save ()
"Save the transcription."
(let ((text (string-trim (buffer-string))))
(remove-hook 'whisper-after-transcription-hook#'my-whisper-org-save)
(erase-buffer) ; stops further processing
(save-window-excursion
(with-current-buffer (marker-buffer whisper--marker)
(goto-char whisper--marker)
(cond
((functionp my-whisper-org-target)
(funcall my-whisper-org-target text))
(my-whisper-org-target
(setq org-capture-initial text)
(org-capture nil my-whisper-org-target)
(org-capture-finalize)
;; Delay the display of the message because whisper--cleanup-transcription clears it
(run-at-time 0.5 nil (lambda (text) (message "Captured: %s" text)) text))
((org-clocking-p)
(let ((link (org-store-link nil)))
(org-clock-goto)
(org-end-of-subtree)
(unless (bolp)
(insert "\n"))
(insert "\n" text "\n" link "\n"))
(run-at-time 0.5 nil (lambda (text) (message "Added clock note: %s" text)) text))
(t
(kill-new text)
(setq org-capture-initial text)
(call-interactively 'org-capture)
;; Delay the window configuration
(let ((config (current-window-configuration)))
(run-at-time 0.5 nil
(lambda (text config)
(set-window-configuration config)
(message "Copied: %s" text))
text config))))))))
Here's an idea for a my-whisper-org-target function that saves the recognized text with a timestamp.
I think I've just figured out my Pipewire setup so
that I can record audio in OBS while also being
able to do speech to text, without the audio
stuttering. qpwgraph was super helpful
for visualizing the Pipewire connections and fixing them.
Actually making a demonstration video will
probably need to wait for another day, though!
I just added two new themes to my ef-themes package. ef-orange is
a light theme with a noticeably tinted background that combines
orange, yellow, and green hues. ef-fig is a dark theme with a dark
purple background that relies on green, yellow, and pink hues.
Both themes are done. Though there may still be some minor
refinements. They will be widely available as part of the next stable
version of the ef-themes package (maybe end of January 2026).
Remember that since version 2.0.0, the ef-themes are built on top
of my modus-themes. This means that they are highly customisable and
support a wide range of packages and face groups.
Enjoy!
The ef-themes are a collection of light and dark themes for GNU
Emacs that provide colourful (“pretty”) yet legible options for users
who want something with a bit more flair than the modus-themes (also
designed by me).
It’s a new year, and that means getting yourself organized with Org Mode. Now, as I’ve covered in the past, I have a love/hate relationship with the Org Mode agenda. But it’s mostly my fault. Just because I have not quite found the right approach to making use of the tool does not mean there’s anything inherently wrong with the tool.
So I’ve been experimenting with using the agenda in different ways—different strategies and approaches, you might say. This has been helpful not only to see what I would like but also what might be of use to others breaking into Org Mode for the first time or curious about its capabilities.
Also, I’m happy to announce that my Git For Writers handbook is finally available. This was my 2025 end-of-year project, and I’m excited to share it with you all. It’s a perfect companion to the Emacs For Writers handbook. So check it out. Thanks!
Also, be sure to check out the video that goes along with this post for a visual demonstration and commentary. Below, you will find the code samples referenced in that video.
Deep Work agenda views
C-c a d : View all deep work tasks
C-c a s : View all shallow work tasks
C-c a w : View complete work type overview (both deep and shallow)
C-c a m : View morning-optimized deep work tasks
C-c a a : View afternoon work tasks
Capture Templates
C-c c d : Capture a deep work task (2:00 effort, intense focus)
C-c c s : Capture a shallow work task (0:30 effort, admin work)
C-c c q : Capture a quick shallow item (0:15 effort)
C-c c r : Capture research/reading task (1:30 effort, deep work)
Agenda views
;; Custom agenda commands for deep work strategy
(setq org-agenda-custom-commands
'(("d""Deep Work Queue"
tags-todo "WORK_TYPE=\"deep\""
((org-agenda-overriding-header "🧠 Deep Work Sessions Available")
(org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-sorting-strategy '(priority-down effort-down))))
("s""Shallow Work Queue"
tags-todo "WORK_TYPE=\"shallow\""
((org-agenda-overriding-header "📋 Shallow Work Tasks")
(org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-sorting-strategy '(priority-down effort-up))))
("w""Work Type Overview"
((tags-todo "WORK_TYPE=\"deep\""
((org-agenda-overriding-header "🧠 Deep Work (Uninterrupted Focus)")
(org-agenda-prefix-format " %i %-12:c [%e] ")))
(tags-todo "WORK_TYPE=\"shallow\""
((org-agenda-overriding-header "📋 Shallow Work (Administrative)")
(org-agenda-prefix-format " %i %-12:c [%e] "))))
((org-agenda-sorting-strategy '(priority-down effort-down))))
("m""Morning Deep Work (High Energy)"
tags-todo "WORK_TYPE=\"deep\"+BEST_TIME=\"morning\""
((org-agenda-overriding-header "🌅 Morning Deep Work Sessions")
(org-agenda-prefix-format " %i %-12:c [%e] ")))
("a""Afternoon Work"
tags-todo "BEST_TIME=\"afternoon\""
((org-agenda-overriding-header "☀️ Afternoon Slump")
(org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-sorting-strategy '(priority-down effort-up))))))
Capture templates
;; Capture templates for deep work task management
(setq org-capture-templates
'(("d""Deep Work Task"
entry
(file+headline "/home/shared/og-deep-work26/deep-work26.org""Tasks")
"* TODO %?\n:PROPERTIES:\n:WORK_TYPE: deep\n:EFFORT: 2:00\n:BEST_TIME: morning\n:END:\n%U\n":empty-lines 1)
("s""Shallow Work Task"
entry
(file+headline "/home/shared/og-deep-work26/deep-work26.org""Tasks")
"* TODO %?\n:PROPERTIES:\n:WORK_TYPE: shallow\n:EFFORT: 0:30\n:BEST_TIME: afternoon\n:END:\n%U\n":empty-lines 1)
("q""Quick Shallow Item"
entry
(file+headline "/home/shared/og-deep-work26/deep-work26.org""Tasks")
"* TODO %?\n:PROPERTIES:\n:WORK_TYPE: shallow\n:EFFORT: 0:15\n:BEST_TIME: anytime\n:END:\n":empty-lines 1)
("r""Research/Reading (Deep)"
entry
(file+headline "/home/shared/og-deep-work26/deep-work26.org""Tasks")
"* TODO %?\n:PROPERTIES:\n:WORK_TYPE: deep\n:EFFORT: 1:30\n:BEST_TIME: morning\n:CATEGORY: research\n:END:\n%U\n":empty-lines 1)))
I’ve been following Sacha Chua for a long time. I think I started while she was still at University but it was certainly shortly afterwards at the latest. She’s been at it for about 25 years so it’s hard for me to remember. During that time I’ve had her in my RSS/Atom feed and read her posts in various feed readers.
These days, of course, I’m using Elfeed but she still pops up regularly in my feed. The other day, I was reading her post on Emacs People for the latest Emacs Carnival and I noticed that she wrote in both French and English. That seemed new so I poked around a bit a discovered that it’s another of her ongoing projects, just like my learning Spanish1. The thing is, I had never seen any other posts in or about French from her. Then I realized that all the posts I’ve been seeing from her were actually coming from Planet Emacslife. What was going on? I checked and, sure enough, her blog was in my feed list. After fiddling around for a bit I tried following the link I had for her in my feed and got a 404 but I had no problem finding the feed directly from her site. I checked it against what her site had and it seemed the same. Then I noticed that on the 404 page there was a %20 at the end of the URL. Aha! There was a space at the end of the URL I was using. I wouldn’t think that would matter but when I removed it, things started working again.
So the moral of this long story is that if you’re having trouble getting the RSS/Atom entries from some site, check to be sure you don’t have a space at the end of the URL. It’s easy to see how it could happen even if you know what you’re doing, and once made, it’s hard to see with a casual glance at things.
Anyway, I’m back to getting all Sacha’s posts now. Too bad that I don’t remember a lot of the French I learned as a result of one of those annoying foreign language requirements in graduate school.
There is a library I rely on constantly in my day to day computing, at work and at home.
This library is called dumb-jump.
I don’t see it is prized as often as Magit for example, which made me write this post.
Below I explain what it does but first, let me thank to Jack Angers and all the contributors.
I have used this library for 10 years. It is one of the best computer programs I have ever used.
There were periods when I was off Emacs during that time and the first thing I looked for in other editors was a plugin similar to dumb-jump.
What does it do
It jumps to definition of a function/method/class.
Here is my config for it:
(use-package dumb-jump
:ensuret:config (add-hook 'xref-backend-functions#'dumb-jump-xref-activate)
(setq dumb-jump-force-searcher 'rg)
;; use completion-read instead of a separate buffer with candidates (setq xref-show-definitions-function #'xref-show-definitions-completing-read))
Combined with rg it eliminated my need in LSPs or ctags/etags entirely.
Here is me using it on Emacs source code, which is big code base (click to view the video in a separate tab):
In this video I:
press M-. to go to definition.
In less than a second I get matching definitions that I can filter if necessary.
I jump straight to the definition.
How does it work
It just greps the entire project for the symbol at point. The best thing about it is that with LSP for example it’s impossible
(or rather I haven’t seen it working) to place my cursor over a function name in a comment somewhere and jump to it.
Like in:
/**
* This test function is cool but
* check another_function_name for more info
*/functiontest() {}
With dumb-jump - I can place my cursor at another_function_name, press M-. and I’m looking at the definition of it.
It works with ANY programming language.
Why not
ctags/etags
Every time a file is changed the ctags table must be regenerated. It can be automated but I think it’s unnecessary overhead.
LSP (Eglot)
I love LSP as a technology but I don’t want a background process to run on my machine while I’m working on a project.
What if during the day I work on 10 projects, which is real for me at work for example. It will spawn 10 background processes
for different languages that I have to mentally track (because I do mentally track what’s running in my computing environment).
The amount of RAM and CPU spent on this just feels too expensive.
Summary
If for some reason you haven’t tried this library - try. It’s so good.
I wish I can find better words on how grateful I am that it exists. Thank you!
Intended audience: This is a long post (~ 8,400
words). It's mostly for me, but I hope it might
also be interesting if you like to use Emacs to do
complicated things or if you also happen to
organize online conferences.
: Added a note about streaming from virtual desktops.
While organizing EmacsConf 2025, I thought it was
going to be a smaller conference compared to last
year because of lots of last-minute cancellations.
Now that I can finally add things up, I see how it
all worked out:
2024
2025
Type
31
25
Presentations
10.7
11.3
Presentation duration (hours)
21
11
Q&A web conferences
7.8
5.2
Q&A duration (hours)
18.5
16.5
Total
EmacsConf 2025 was actually a little longer than 2024 in total presentation time, although that's probably because we had more live talks which included answering questions.
It was almost as long overall including live Q&A in BigBlueButton rooms, but we did end an hour or so earlier each day.
Looking at the livestreaming data, I see that we had fewer participants compared to the previous year. Here are the stats from Icecast, the program we use for streaming:
Saturday:
gen: 107 peak + 7 lowres (compared to 191 in 2024)
dev: 97 peak + 7 lowres (compared to 305 in 2024)
Sunday: I forgot to copy Sunday stats, whoops! I think there were about 70 people on the general stream. Idea: Automate this next time.
The YouTube livestream also had fewer participants at the time of the stream, but that's okay. Here are the stats from YouTube:
Fewer people attended compared to last year, but it's still an amazing get-together from the perspective of being able to get a bunch of Emacs geeks in a (virtual) room. People asked a lot of questions over IRC and the Etherpads, and the speakers shared lots of extra details that we captured in the Q&A sessions. I'm so glad people were able to connect with each other.
Based on the e-mails I got from speakers about their presentations and the regrets from people who couldn't make it to EmacsConf, it seemed that people were a lot busier in 2025 compared to 2024. There were also a lot more stressors in people's lives. But it was still a good get-together, and it'll continue to be useful going forward.
At the moment, the EmacsConf 2025 videos have about 20k views total on YouTube. (media.emacsconf.org doesn't do any tracking.) Here are the most popular ones so far:
While I was looking at the viewing stats on YouTube, I noticed that people are still looking at videos all the way back to 2013 (like Emacs Live - Sam Aaron and Emacs Lisp Development - John Wiegley), and many videos have thousands of views. Here are some of the most popular ones from past conferences:
Views aren't everything, of course, but maybe they let us imagine a little about how many people these speakers might have been able to reach. How wonderful it is that people can spend a few days putting together their insights and then have that resonate with other people through time. Speakers are giving us long-term gifts.
Timeline
Of course, the process of preparing all of that started long before the days of the conference. We posted the call for proposals towards the end of June, like we usually do, because we wanted to give people plenty of time to start thinking about their presentations. We did early acceptances again this year, and we basically accepted everything, so people could start working on their videos almost right away. Here's the general timeline:
CFP
2025-06-27 Fri
CFP deadline
2025-09-19 Fri
Speaker notifications
2025-09-26 Fri
Publish schedule
2025-10-24 Fri (oops, forgot to do this elsewhere)
Video submission target date
2025-10-31 Fri
EmacsConf
2025-12-06 Sat - 2025-12-07 Sun
Live talks, Q&A videos posted
2025-12-17
Q&A notes posted
2025-12-28
These notes
2026-01-01
Figure 1: EmacsConf 2025: cumulative proposals and video uploads
This graph shows that we got more proposals earlier this year (solid blue line: 2025) compared to last year (gray: 2024), although there were fewer last-minute ones and more cancellations this year. Some people were very organized. (Video submissions in August!) Some people sent theirs in later because they first had to figure out all the details of what they proposed, which is totally relatable for anyone who's found themselves shaving Emacs yaks before.
I really appreciated the code that I wrote to create SVG previews of the schedule. That made it much easier to see how the schedule changed as I added or removed talks. I started stressing out about the gap between the proposals and the uploaded videos (orange) in November. Compared to last year, the submissions slowly trickled in. The size of the gap between the blue line (cumulative proposals) and the orange line (cumulative videos uploaded) was much like my stress level, because I was wondering how I'd rearrange things if most of the talks had to cancel at the last minute or if we were dealing with a mostly-live schedule. Balancing EmacsConf anxiety with family challenges resulted in all sorts of oopses in my personal life. (I even accidentally shrank my daughter's favourite T-shirt.) It helped to remind myself that we started off as a single-track single-day conference with mostly live sessions, that it's totally all right to go back to that, and that we have a wonderful (and very patient) community. I think speakers were getting pretty stressed too. I reassured speakers that Oct 31 was a soft target date, not a hard deadline. I didn't want to cancel talks just because life got busy for people.
It worked out reasonably well. We had enough capacity to process and caption the videos as they came in, even the last-minute uploads. Many speakers did their own captions. We ended up having five live talks. Live talks are generally a bit more stressful for me because I worry about technical issues or other things getting in the way, but all those talks went well, thank goodness.
For some reason, Linode doesn't seem to show me detailed accrued charges any more. I think it used to show them before, which is why I managed to write last year's notes in December. If I skip the budget section of these notes, maybe I can post them earlier, and then just follow up once the invoices are out.
I think the timeline worked out mostly all right this year. I don't think moving the target date for videos earlier would have made much of a difference, since speakers would probably still be influenced by the actual date of the conference. It's hard to stave off that feeling of pre-conference panic: Do we have enough speakers? Do we have enough videos? But these graphs might help me remember that it's been like that before and it has still worked out, so it might just be part of my job as an organizer to embrace the uncertainty. Most conferences do live talks, anyway.
Managing conference information in Org Mode and Emacs
Organizing the conference meant keeping track of lots of e-mails and lots of tasks over a long period of time. To handle all of the moving parts, I relied on Org
Mode in Emacs to manage all the conference-related information.
This year, I switched to mainly using the general
organizers notebook, with the year-specific one
mostly just used for the draft schedule.
I added some shortcuts to jump to headings in the
main notebook or in the annual notebook (emacsconf-main-org-notebook-heading and emacsconf-current-org-notebook-headingemacsconf.el).
As usual, we used lots of structured entry properties to
store all the talk information.
Most of my functions from last year worked out
fine, aside from the occasional odd bug when I
forgot what property I was supposed to use. (:ROOM:, not :BBB:…)
The validation functions I thought about last year
would have been nice to have, since I ran into a
minor case-sensitivity issue with the Q&A value
for one of the talks (live versus Live).
This happened last year too and it should have
been caught by the case-fold-search I thought I
added to my configuration, but since that didn't
seem to kick in, I should probably also deal with
it on the input side.
Idea: I want to validate that certain fields
like QA_TYPE have only a specified set of
values.
I also probably need a validation function to make
sure that newly-added talks have all the files
that the streaming setup expects, like an overlay
for OBS, and one that checks if I've set any
properties outside the expected list.
Once the speakers confirmed the tentative schedules worked for them, I published the schedule on emacsconf.org on Oct 24, as sceduled. But I forgot to announce the schedule on emacsconf-discuss and other places, though. I probably felt a little uncertain about announcing the schedule because it was in such flux. There was even a point where we almost cancelled the whole thing. I also got too busy to reach out to podcasts. I remembered to post it to foss.events, though! Idea: I can announce things and trust that it'll all settle down. I can spend some time neatening up our general organizers-notebook so that there's more of a step-by-step-process. Maybe org-clone-subtree-with-time-shift will be handy.
There were also a few posts and threads afterwards:
For communicating with speakers and volunteers, I reused the mail merge from previous years, with a few more tweaks. I added a template for thanking volunteers.
I added some more code for mailing all the volunteers with a specific tag.
Some speakers were very used to the process and did everything pretty independently.
Other speakers needed a bit more facilitation.
I could get better at coordinating with people who want to help. In the early days, there wasn't that much to help with. A few people volunteered to help with captions for specific videos, so I waited for their edits and focused on other tasks first. I didn't want to pressure them with deadlines or preempt them by working on those when I'd gotten through my other tasks, but it was hard to leave those tasks dangling. They ended up sending in the edits shortly before the conference, which still gave me enough time to coordinate with speakers to review the captions. It was hard to wait, but I appreciate the work they contributed. In late November and early December, there were so many tasks to juggle, but I probably could have e-mailed people once a week with a summary of the things that could be done. Idea: I can practice letting people know what I'm working on and what tasks remain, and I can find a way to ask for help that fits us. If I clean up the design of the backstage area, that might make it easier for people to get started. If I improve my code for comparing captions, I can continue editing subtitles as a backup if I want to take a break from my other work, and merge in other people's contributions when they send them.
I set up Mumble for backstage coordination with other organizers, but we didn't use it much because most of the time, we were on air with someone, so we didn't want to interrupt each other.
There was a question about our guidelines for conduct and someone's out-of-conference postings. I forwarded the matter to our private mailing list so that other volunteers could think about it, because at that point in time, my brain was fried. That seemed to have been resolved. Too bad there's no edebug-on-entry for people so we can figure things out with more clarity. I'm glad other people are around to help navigate situations!
Schedule
Like last year, we had two days of talks, with two tracks on the first day, and about 15-20 minutes between each talk.
We had a couple of last-minute switches to live Q&A sessions, which was totally fine.
That's why I set up all the rooms in advance.
A few talks ended up being a bit longer than their
proposed length.
With enough of a heads-up, I could adjust the
schedule (especially as other talks got
cancelled), but sometimes it was difficult to keep
track of changes as I shuffled talks around.
I wrote some code to calculate the differences, and
I appreciate how the speakers patiently dealt with
my flurries of e-mails.
Scheduling some more buffer
time between talks might be good. In general, the
Q&A time felt like a good length, though, and it
was nice that people had the option of continuing
with the speaker in the web conference room. So it
was actually more like we had two or three or four
tracks going on at the same time.
Having two tracks allowed us to accept all the
talks. I'm glad I kept the dev track to a single
day, though. I ended up managing things all by
myself on Sunday afternoon, and that was hard
enough with one track. Fortunately, speakers were
comfortable navigating the Etherpad questions themselves and reading the questions out loud. Sometimes I stepped in during natural pauses to read the next question on the list
(assuming I managed to find the right window among all
the ones open on my screen).
Idea: If I get better at setting up one window per ongoing Q&A session and using the volume controls to spatialize each so that I can distinguish left/middle/right conversations, it might be easier for me to keep track of all of those. I wasn't quite sure how to arrange all the windows I wanted to pay attention even with an external screen (1920x1200 + my laptop's 1920x1080). I wonder if I'd trust EXWM to handle tiling all the different web browser windows, while keeping the VNC windows floating so that they don't mess with the size of the stream. Maybe I can borrow my husband's ultrawide monitor. Maybe I can see if I can use one of those fancy macropads for quick shortcuts, like switching to a specified set of windows and unmuting myself. Maybe I can figure out how to funnel streaming captions into IRC channels or another text form (also a requested feature) so that I can monitor them that way and quickly skim the history… Or I can focus on making it easier for people to volunteer (and reminding myself that it's okay to ask for their help!), since then I don't have to split my attention all those different ways and I get to learn from other people's awesomeness.
If we decide to go with one track next year, we
might have to prioritize talks, which is hard
especially if some of the accepted speakers end up
cancelling anyway. Alternatively, a better way
might be to make things easier for last-minute
volunteers to join and read the questions. They
don't even need any special setup or anything;
they can just join the BigBlueButton web conference session and read from
the pad. Idea: A single Etherpad with all the
direct BBB URLs and pad links will make this
easier for last-minute volunteers.
Also like last year, we added an open mic session to fill in the time from a last-minute cancellation, and that went well. It might be nice to build that into the schedule earlier instead of waiting for a cancellation so that people can plan for it. We moved the closing remarks earlier as well so that people didn't have to stay up so late in other timezones.
I am super, super thankful we had a crontab automatically switching between talks, because that meant one less time-sensitive thing I had to pay attention to. I didn't worry about cutting people off too early because people could continue off-stream, although this generally worked better for Q&A sessions done over BigBlueButton or on Etherpad rather than IRC. I also improved the code for generating a test schedule so that I could test the automatic switching.
It was probably a good thing that I didn't automatically write the next session time to the Etherpad, though, because people had already started adding notes and questions to the pad before the conference, so I couldn't automatically update them as I changed the schedule. Idea: I can write a function to copy the heads-up for the next talk, or I can add the text to a checklist so that I can easily copy and paste it. Since I was managing the check-ins for both streams as well as doing the occasional bit of hosting, a checklist combining all the info for all the streams might be nice. I didn't really take advantage of the editability of Etherpad, so maybe putting it into HTML instead will allow me to make it easier to skim or use. (Copy icons, etc.)
Like before, I offset the start of the dev track to give ourselves a little more time to warm up, and I started Sunday morning with more asynchronous Q&A instead of web conferences. Not much in terms of bandwidth issues this year.
We got everyone's time constraints correctly this year, hooray!
Doing timezone conversions in Emacs means I don't have to calculate things myself, and the code for checking the time constraints of scheduled sessions worked with this year's shifting schedules too.
Idea: It would be great to mail iCalendar files (.ics) to each speaker so that they can easily add their talk (including check-in time, URLs, and mod codes) to their calendar.
Bonus points if we can get it to update previous copies if I've made a change.
I added a vertical view to the 2025 organizers notebook, which was easier to read. I also added some code to handle cancelling a talk and keeping track of rescheduled talks.
Idea: I still haven't gotten around to localizing times on the watch pages. That could be nice.
Recorded introductions
Recording all the introductions beforehand was extremely helpful. I used subed-record.el to record and compile the audio without having to edit out the oopses manually. Recording the intros also gave me something to do other than worry about missing videos.
A few speakers helped correct the pronunciations of their names, which was nice. I probably could have picked up the right pronunciation for one of them if I had remembered to check his video first, as he had not only uploaded a video early but even said his name in it. Next time, I can go check that first.
This year, all the intros played the properly corrected files along with their subtitles. The process is improving!
Recorded videos
As usual, most speakers sent in pre-recorded
videos this year, although it took them a little
bit longer to do them because life got busy for
everyone. Just like last year, speakers uploaded
their files via PsiTransfer. I picked up some
people's videos late because I hadn't checked.
I've modified the upload instructions to ask the
speakers to email me when they've uploaded their
file, since I'm not sure that PsiTransfer can
automatically send email when a new file has been
uploaded. We set up a proper domain name, so this
year, people didn't get confused by trying to FTP
to it.
I had to redo some of the re-encodings and ask one speaker to reupload is video, but we managed to catch those errors before they streamed.
I normalized all the audio myself this year. I used Audacity to normalize it to -16 LUFS. I didn't listen to everything in full, but the results seemed to have been alright. Idea: Audio left/right difference was not an issue this year, but in the future, I might still consider mixing the audio down to mono.
All the files properly loaded from the cache directory instead of getting shadowed by files in other directories, since I reused the process from last year instead of switching mid-way.
People didn't complain about colour smearing, but
it looks like the Calc talk had some. Ah! For some
reason, MPV was back on 0.35, so now I've modified
our Ansible playbook so that it insists on 0.38. I
don't think there were any issues about switching
between dark mode or light mode.
There was one last-minute upload where I wasn't sure whether there were supposed to be captions in the first part. When I toggled the captions to try to reload them once I copied over the updated VTT, I think I accidentally left them toggled them off, but fortunately the speaker let me know in IRC.
For the opening video, I made the text a bit more generic by removing the year references so that I can theoretically reuse the audio next year.
That might save me some time.
I modified our mpv.conf to display the time remaining in the lower right-hand corner. This was a fantastic idea that made it easy to give the speaker a heads-up that their recorded talk was about to finish and that we were about to switch over to the live Q&A session. Because sometimes we weren't able to spare enough attention to host a session, I usually just added the time of the next session to the Etherpad to give speakers a heads-up that the stream would be switching away from their Q&A session. Then I didn't need to make a fancy Javascript timer either.
Captioning
The next step in the process was to caption each uploaded video. While speech recognition tools give us a great headstart, there's really no way around the work of getting technical topics right.
We used WhisperX for speech-to-text again. This time, I used the --initial_prompt argument
to try to get it to spell things properly: "Transcribe this talk about Emacs. It may mention Emacs keywords such as Org Mode, Org Roam, Magit, gptel, or chatgpt-shell, or tech keywords such as LLMs. Format function names and keyboard shortcut sequences according to Emacs conventions using Markdown syntax. For example: control h becomes \`C-h\`.". It did not actually do the keyboard shortcut sequences. (Emacs documentation is an infinitesimal fraction of the training data, probably!) It generally did a good job of recognizing Emacs rather than EMAX and capitalizing things. Idea: I can experiment with few-shot prompting techniques or just expand my-subed-fix-common-errors to detect the patterns.
This year, we used Anush V's sub-seg tool to split the subtitles into reasonably short, logically split phrases. I used to manually split these so that the subtitles flowed more smoothly, or if I was pressed for time, I left it at just the length-based splits that WhisperX uses. sub-seg was much nicer as a starting point, so I wrote a shell script to run it more automatically. It still had a few run-on captions and there were a couple of files that confused it, so I manually split those.
I felt that it was also a good idea to correct the timing before posting the subtitles for other people to work on because otherwise it would be more difficult for any volunteers to be able to replay parts of the subtitles that needed extra attention. WhisperX often gave me overlapping caption timestamps and it didn't always have word timestamps I could use instead. I used Aeneas to re-align the text with the audio so that we could get better timestamps. As usual, Aeneas got confused by silences or non-speech audio, so I used my subed-word-data.el code to visualize the word timing and my subed-align.el code to realign regions. Idea: I should probably be able to use the word data to realign things semi-automatically, or at least flag things that might need more tweaking. There were a few chunks that weren't recognized at all, but I was able to spot them and fix them when I was fixing the timestamps. Making this more automated will make it much easier to share captioning work with other volunteers. Alternatively, I can also post the talks for captioning before I fix the timestamps, because I can fix them while other people are editing the text.
While I was correcting the timestamps, it was pretty tempting for me to just go ahead and fix whatever errors I encountered along the way. From there on, it seemed like only a little bit more work was needed in order to get it ready for the speaker to review, and besides, doing subtitles is one of the things I enjoy about EmacsConf. So the end result is that for most of the talks, by the time I finished getting it to the point where I felt like someone who was new to captioning could take over, it was often quite close to being done. I did actually manage to successfully accept some people's help. The code that I wrote for showing me the word differences between two files (sometimes the speaker's script or the subtitles that were edited by another volunteer) was very useful for last-minute merges.
Working on captions is one of my favourite parts. Well-edited captions are totally a nice-to-have, but I like it when people can watch the videos (or skim them) and not worry about not being able to hear or understand what someone said. I enjoy sitting down and spending time with people's presentations, turning them into text that we can search. Captioning helps me feel connected with some of the things I love the most about the Emacs community.
I really appreciated how a number of speakers and volunteers helped with quality control by watching other videos in the backstage area. Idea: I can spend some time improving the design of the backstage area to make it more pleasant and to make it easier for people to find something to work on if they have a little time.
Of course, some talks couldn't be captioned beforehand because they were live. For live conferences and for many of the Q&A sessions, we used BigBlueButton.
BigBlueButton web conference
We used BigBlueButton again this year instead of switching over to something like Galene. I moved the node from bandali's Linode account to mine so that I wouldn't feel as guilty bringing it up and down throughout the year for various Emacs meetups. That also meant that the actual setup was fairly stable since it had survived a lot of meetups before EmacsConf, and I didn't have to spend as much time before the event worrying about it. I still worried a little, as after the November OrgMeetup, I noticed a technical issue that I couldn't nail down. Fortunately, that happened after the meetup had already ended. I also used my meetup crontab to schedule test sessions with some speakers so that they could record their presentation or doublecheck their sharing setup.
I resized the BigBlueButton early Friday afternoon this year so that speakers could have a few more hours to test their setups if they wanted to, especially since one speaker had mentioned having a hard time sharing his screen on Wayland.
I thought I had --allow_auto_disk_resize true in the bbb-prod shell-script I used to resize the node, but I don't think linode-cli actually resized the disk (maybe I ran a different shell script), and I forgot to double-check that before the conference. Fortunately, we had just enough space for all the recordings. I noticed only when I was waiting for the recordings to finish processing, since some of them were failing. Idea: Next time, I should manually check that the disk has resized, and I can probably tweak my checklist too.
Like before, we used moderator codes so that speakers could let themselves into the room and get everything set up just in case. Idea: I wonder if there's a way to specify the moderator code in the URL so that I can link to that directly. Maybe I can modify the web interface to fill that in with a little bit of Javascript.
Oddly, the moderator codes didn't let people start or stop recording, but fortunately I was able to log in and take care of all of that. I mostly remembered to start the recording, except for one instance where I started recording a little bit late.
I might be able to get away with a Linode 8 GB node (USD 0.072/hour) instead of a Linode 16 GB node (USD 0.144/hour), but keeping it at the current size is probably okay.
I'll go into more detail in the budget section, but the total was USD 14.02 to host BigBlueButton ourselves in December, and between USD 5 to USD 8 / month to run it the rest of the time (including upsizing it for meetups), so that was much less than what it would have cost for BigBlueButton-specific hosting.
Checking speakers in
To keep the live talks and Q&A sessions flowing smoothly, there's a bit of a scramble behind the scenes. We asked speakers to check in at least half an hour before they need to go live, and most of them were able to find their way to the BigBlueButton room using the provided moderator codes. I mostly handled the check-ins, with some help from Corwin when two people needed to be checked in at the same time. We generally didn't have any tech issues, although a couple of people were missing during their Q&A sessions. (We just shrugged and continued; too much to take care of to worry about anything!)
Check-ins are usually good opportunities to chat with speakers, but because I needed to pay attention to the other streams as well as check in other people, it felt a bit more rushed and less enjoyable. I missed having those opportunities to connect, but I'm glad speakers were able to handle so much on their own.
Hosting
Corwin did a lot of the on-stream hosting with a little help from Amin on Saturday. I occasionally jumped in and asked the speakers questions from the pad, but when I was busy checking people in or managing other technical issues cropping up, the speakers also read the questions out themselves so that I could match things up in the transcript afterwards.
No crashes this time, I think. Hooray!
Infrastructure
I used September and October to review all the infrastructure and see what made sense to upgrade. The BigBlueButton instance is on a virtual private server that's dedicated to it, since it doesn't like to share with any other services. Since I set it up from scratch following the recommended configuration, the server uses a recent version of Ubuntu. Some of the other servers use outdated Debian distributions that no longer get security updates. They have other services on them, so I'll leave them to bandali to upgrade when he's ready.
Streaming
I am so, so glad that we had our VNC+OBS setup this year, with two separate user accounts using OBS to stream each track from a virtual desktop on a remote server. We switched to this setup in 2022 so that I could handle multiple tracks without worrying about last-minute coordination or people's laptop specs. If we had had our original setup in 2019, with hosts streaming from their personal computers, I think we'd have been dead in the water. Instead, our scripts took care of most of the on-screen actions, so I just needed to rearrange the layout. (Idea: If I can figure out how to get BigBlueButton to use our preferred layout, that would make it even better.)
I used MPV to monitor the streams in little thumbnails. I set those windows to always be on top, and I set the audio so that the general track was on my left side and the development track was o my right. I used some shortcuts to jump between streams reliably, taking advantage of how I bound the Windows key on my laptop to the Super modifier. I used KDE's custom keyboard shortcuts to set Super-g to raise all of my general-track windows and Super-d to do the same for all of the development-track ones. Those were set to commands like this:
if ; then
if ; then
"$Executable" > /dev/null 2>&1 &
else
$LaunchCommand > /dev/null 2>&1 &
fi
disown
else
if xdotool search –class "\(ExecutableBase" | grep -q "^\)(xdotool getactivewindow)$"; then
xdotool sleep 0.050 key "ctrl+alt+l"
else
xdotool windowactivate "$MostRecentWID" 2>&1 | grep failed \
&& xdotool search –class "$ExecutableBase" windowactivate %@
fi
fi
(Idea: It would be great to easily assign specific windows to shortcuts on my numeric keypad or on a macropad. Maybe I can rename a window or manually update a list that a script can read…)
To get the video streams out to viewers, we used Icecast (a streaming media server) on a Linode 64GB 16 core shared CPU server again this year, and that seemed to work out. I briefly contemplated using a more modern setup like nginx-rtmp, Ant Media Server, or SRS if we wanted HLS (wider support), adaptive bitrate streaming, or lower latency, but I decided against it because I didn't want to add more complexity. Good thing too. This year would not have been a good time to experiment with something new.
Like before, watching the stream directly using mpv, vlc, or ffplay was smoother than watching it through the web-based viewers. One of the participants suggested adding more detailed instructions for VLC so that people can enjoy it even without using the command-line, so we did.
The 480p stream and the YouTube stream were all right, although I forgot to start one of the YouTube streams until a bit later. Idea: Next time, I can set all the streams to autostart.
Corwin had an extra server lying around, so I used that to restream to Toobnix just in case. That seems to have worked, although I didn't have the brainspace to check on it.
I totally forgot about displaying random packages on the waiting screen. Idea: Maybe next year I can add a fortune.txt to the cache directory with various one-line Emacs tips.
I might be able to drop the specifications down to 32GB 8 core if we wanted to.
Publishing
People appreciated being able to get videos and transcripts as soon as each talk aired. Publishing the video files and transcripts generally worked out smoothly, aside from a few times when I needed to manually update the git repository. I modified the code to add more links to the Org files and to walk me through publishing videos to YouTube.
I uploaded all the videos to YouTube and scheduled them so that I didn't have to manage that during the conference. I did not get around to uploading them to Toobnix until after the conference since dealing with lots of duplicated updates is annoying. I've been writing a few more functions to work with the Toobnix API, so it might be a little easier to do things like update descriptions or subtitles next year.
Etherpad
As usual, we used Etherpad to collect questions from conference partcipants, and many speakers answered questions there as well.
I used the systemli.etherpad Ansible role to upgrade to Etherpad 2.5. This included some security fixes, so that was a relief.
I added pronouns and pronunciations to the Etherpad to make it easier for hosts to remember just in case.
I did not get to use emacsconf-erc-copy to copy IRC to Etherpad because I was too busy juggling everything else, so I just updated things afterwards.
Idea: Just in case, it might be good to have a backup plan in case I need to switch Etherpad to authenticated users or read-only use. Maybe I can prepare questions beforehand, just in case we get some serious griefing.
IRC
The plain-text chat channels on IRC continued to be a great place to discuss things, with lots of discussions, comments, and encouraging feedback. My automated IRC system continued to do a good job of posting the talk links, and the announcements made it easier to split up the log by talk.
Planning for social challenges is harder than planning for technical ones, though.
libera.chat has been dealing with spam attacks recently. Someone's also been griefing #emacs and other channels via the chat.emacsconf.org web interface, so we decided to keep chat.emacsconf.org dormant until the conference itself. If this is an issue next year, we might need to figure out moderation. I'd prefer to not require everyone to register accounts or be granted voice permissions, so we'll see.
Extracting the Q&A
We recorded all the Q&A sessions so that we could post them afterwards. As mentioned, I only started the recording late once. Progress! I generally started it a few minutes early. As I got more confident about paying attention to the start of a session and rearranging the layout on screen, I also got better at starting the recording shortly before turning it over to the speaker. That made it easier to trim the recordings afterwards.
It took me a little longer to get to the Q&A sessions this year. Like last year, getting the recordings from BigBlueButton was fairly easy because I could get a single processed video instead of combining the audio, screenshare, and webcam video myself. This year the single-file video downloads were .m4v files, so I needed to modify things slightly. I think my workflow last year assumed I started with .webm files. Anyway, after some re-encoding, I got it processed. Now I've modified the /usr/local/bigbluebutton/core/scripts/video.yml to enable webm. I wonder if it got wiped after my panic-reinstall after November's OrgMeetup. Idea: I should add the config to my Ansible so that it stays throughout reinstalls and upgrades.
I wrote a emacsconf-subed-copy-current-chapter-text function so that I can more easily paste answers into the Q&A… which turned out to be a superset of the emacsconf-extract-subed-copy-section-text I mentioned in last year's notes and totally forgot about. This tells me that I need to add it to my EmacsConf - organizers-notebook notes so that I actually know what to do. I did not use my completion functions to add section headings based on comments either. Apparently this was emacsconf-extract-insert-note-with-question-heading, which I do have a note about in my organizer notebook.
Audio mixing was reasonable. I normalized the audio in Audacity, and I manually fixed some sections where some participants' audio volumes were lower.
Budget
Hosting a conference online continues to be pretty manageable. Here are our costs for the year (including taxes where applicable):
peak 60% total CPU (100% is 12 CPUs); each OBS ~3.5 CPUs), mem 7GB used
media
3GB 1core
These hosting costs are a little higher than 2024 because we now pay for hosting the BigBlueButton server (meet) year-round. That's ~ 5 USD/month for a Linode 1 GB instance and an extra USD 2-3 / month that lets us provide a platform for OrgMeetup, Emacs APAC, and Emacs Berlin by alternating between a 1 GB Linode and an 8 GB Linode as needed. The meetups had previously been using the free Jitsi service, but sometimes that has some limitations. In 2023 and most of 2024, the BigBlueButton server had been provided by FossHost, which has since shut down. This time, maintaining the server year-round meant that we didn't have to do any last-minute scrambles to install and configure the machine, which I appreciated. I could potentially get the cost down further if I use Linode's custom images to create a node from a saved image right before a meetup, but I think that trades about USD 2 of savings/month for much more technical risk, so I'm fine with just leaving it running downscaled.
If we need to cut costs, live0 might be more of a candidate because I think we'll be able to use Ansible scripts to recreate the Icecast setup. I think we're fine, though. Also, based on the CPU peak loads, we might be able to get away with lower specs during the conference (maybe meet: 8 GB, front: 8 GB, live: 32 GB), for an estimated savings of USD 27.76, with the risk of having to worry about it if we hit the limit. So it's probably not a big deal for now.
I think people's donations through the Working Together program can cover the costs for this year, just like last year. (Thanks!) I just have to do some paperwork.
In addition to these servers, Ry P provided res.emacsconf.org for OBS streaming over VNC sessions.
The Free Software Foundation also provided media.emacsconf.org for serving media files, and yang3 provided eu.media.emacsconf.org.
If other people are considering running an online conference, the hosting part is surprisingly manageable, at least for our tiny audience size of about 100 peak simultaneous viewers and 35 web conference participants. It was nice to make sure that everyone can watch without ads or Javascript.
Behind the scenes: In terms of keeping an eye on performance limits, we're usually more CPU-bound than memory- or disk-bound, so we had plenty of room this year. Now I have some Org Babel source blocks to automatically collect the stats from different servers. Here's what that Org block looks like:
I should write something similar to grab the Icecast stats from http://live0.emacsconf.org:8001/status.xsl periodically. Curl will do the trick, of course, but maybe I can get Emacs to add a row to an Org Mode table.
Tracking my time
As part of my general interest in time-tracking, I tracked EmacsConf-related time separately from my general Emacs-related time.
This year, I spent more time doing the reencoding and captions in December, since most of the submissions came in around that time. I didn't even listen to all the videos in real time. I used my shortcuts to skim the captions and jump around. It would have been nice to be able to spread out the work a little bit more instead of squeezing most of it into the first week of December, since that would have made it easier to coordinate with other volunteers without feeling like I might have to do more last-minute scrambles if they were busier than expected.
I was a little bit nervous about making sure that the infrastructure was all going to be okay for the conference as well as double checking the videos for possible encoding issues or audio issues.
Stress tends to make me gloss over things or make small mistakes, which meant I had to slow down even more to double-check things and add more things to our process documentation.
I'm glad it all worked out. Even if I switched that time to working on Emacs stuff myself, I don't think I would have been able to put together all the kinds of wonderful tips and experiences that other people shared in the conference, so that was worthwhile.
Thanks to people who donated via the FSF Working Together program: Scott Ranby, Jonathan Mitchell, and 8 other anonymous donors!
Overall
I've already started hearing from people who
enjoyed the conference and picked up lots of good
tips from it. Wonderful!
EmacsConf 2025 was a bit more challenging this
year. The world has gotten a lot busier.
Return-to-work mandates, job market turmoil,
health challenges, bigger societal changes… It's
harder for people to find time. For example, the
maintainers of Emacs and Org are too busy working
on useful updates and bugfixes to rehash the news
for us, so maybe I'll get back to doing Emacs News
Highlights next year. I'm super lucky in that I
can stand outside most of all of that and make
this space where we can take a break and chat
about Emacs. I really appreciated having this
nice, cozy little place where people could get
together and bump into other people who might be
interested in similar things, either at the event
itself or in the discussions afterwards. I'm glad
we started with a large schedule and let things
settle down. I'd rather have that slack instead of
making speakers feel stressed or guilty.
I love the way that working on the various parts
of the conference gives me an excuse to tinker with Emacs and figure out how to use it for more things.
As you can tell from the other sections in this
post, I tend to have so much fun doing this that I
often forget to check if I've already written the
same functions before.
When I do remember, I feel really good about being
able to build on this accumulation of little
improvements.
I didn't feel like I really got to attend EmacsConf this year, but that's not really surprising because I don't usually get to. I think the only time I've actually been able to take proper notes during EmacsConf itself was back in 2013. It's okay, I get to spend a little time with presentations before anyone else does, and there's always the time afterwards. Plus I can always reach out to the speakers myself. It might be nice to be able to just sit and enjoy it and ask questions. Maybe someday when I've automated enough to make this something that I can run easily even on my own.
So let's quickly sketch out some possible scenarios:
I might need to do it on my own next year, in case other organizers get pulled away at the last minute: I think it's possible, especially if I can plan for some emergency moderation or last-minute volunteers. I had a good experience, despite the stress of juggling things live on stream. (One time, one of the speakers had a question for me, and he had to repeat it a few times before I found the right tab and unmuted.) Still, I think it would be great to do it again next year.
Some of the other organizers might be more available: It would be nice
to have people on screen handling the hosting. If people can help out with verifying the encoding, normalizing the audio, and shepherding videos through the process, that might let me free up some time to coordinate captions with other volunteers even for later submissions.
I might get better at asking for help and making it easier for people to get involved: That would be pretty awesome. Sometimes it's hard for me to think about what and how, so if some people can take the initiative, that could be good.
This is good. It means that I can probably say yes to EmacsConf even if it's just me and whoever wants to share what they've been learning about Emacs this year. That's the basic level. We're still here, amazing! Anything else people can add to that is a welcome bonus. We'll make stone soup together.
EmacsConf doesn't have to be snazzy. We don't need to try to out-market VS Code or whatever other editors emerge over the next 10 years. We can just keep having fun and doing awesome things with Emacs. I love the way that this online conference lets people participate from all over the world. We like to focus on facilitating sharing and then capturing the videos, questions, answers so that people can keep learning from them afterwards. I'm looking forward to more of that next year.
I'd love to hear what people thought of it and see how we can make things better together. I'd love to swap notes with organizers of other conferences, too. What's it like behind the scenes?
Jack Baty has a New Year’s resolution: Don’t change blogging platforms more than once a quarter. Say what? I don’t know about the rest of you but changing blogging platforms regularly seems to me like getting divorced and remarried regularly. What sensible person would want to do that? Baty’s excuse is that he likes to tinker and mostly blogs about tinkering so naturally changing blogging platforms seems to make sense to him.
For me, blogging is all about writing and sharing my discoveries. The last thing I want is to worry about is my blogging platform. I want it to be as transparent as possible so I don’t have to think about it. I just want to write my post in Org mode and push a button to publish it.
I started blogging with Blogger. It was easy and it wasn’t too hard to turn an Org mode file into a post. After a while I got my own domain, Irreal.org, and moved to WordPress. It’s not all that different from Blogger—except that Google isn’t lurking in the background deciding whether my posts are acceptable—and, like blogger, it’s easy to publish Org files as posts.
It’s been 14 years since I moved to WordPress and while it’s sometimes a pain, I’ve never seriously considered moving to something else.
There are, it seems to me, two type of blogging platforms: static and database-centric. Static blogs are simple and don’t require backups but they require more work on the front end. Database systems, like Blogger and WordPress, are more turnkey but are more susceptible to exploits and require you to backup the database periodically.
I don’t know which is the best—it probably depends on your inclinations—but once you’ve decided on a platform you should probably stick with it unless there are compelling reasons to change.
WordPress certainly isn’t perfect but it’s good enough. It allows me to concentrate on my writing and not worry about the details of publishing it. Unless things change drastically, I don’t see Irreal changing.
How about this for a resolution… Don’t change blogging platforms more than once a quarter.
Ha!
I sometimes wish playing with tools wasn’t so much fun. It would be better, I think, to write more, tinker less. Except that I mostly write about tinkering, so that’s sort of self-defeating, no?
Let’s recap.
2025 was comprised of Kirby, Ghost, WordPress, Hugo, Zola, Eleventy, Blot, Tinderbox, Emacs, and TiddlyWiki. That seems like a lot, even for me.
That’s too much to manage. I dream of having One True Blog, but we all know that’s not happening. Going into 2026, I’m going to try and limit it to 3. This blog, the daily blog, and the wiki. I sometimes wonder why I have both the wiki and daily blogs, but I have never been able to settle on one or the other.
Three blogs seems like plenty to keep me busy. If I can figure out how to not change how they’re made every other day1, we should be good.
I have created another package!, this time something that I thought was missing from the mighty Emacs and that is the ability to show video thumbnails in a grid and to be able to filter, sort e.t.c. Basically like an enhanced image-dired. I have been increasingly using image-dired for my image editing and management needs and am always adding little improvements, to such an extent I decided to create a video thumb grid package, enjoy!
Introduction
dired-video-thumbnail is an Emacs package that provides image-dired style thumbnail viewing for video files. It extracts thumbnails from videos using ffmpeg and displays them in a grid layout, allowing you to visually browse and manage video collections directly from Emacs.
Features
Thumbnail grid display - View video thumbnails in a configurable grid layout
Persistent caching - Thumbnails are cached and only regenerated when the source file changes
Async generation - Emacs remains responsive while thumbnails are generated in the background
Dired integration - Marks sync bidirectionally with the associated dired buffer
Visual mark indication - Marked thumbnails display a coloured border (like image-dired)
Dynamic header line - Shows filename, dimensions, duration, and file size for the current video
Click to play - Open videos in your preferred external player
Cross-platform - Works on Linux, macOS, and Windows
Resizable thumbnails - Adjust thumbnail size on the fly
Sorting - Sort videos by name, date, size, or duration
Filtering - Filter videos by name pattern, duration range, or file size
Recursive search - Browse videos across subdirectories with optional auto-recursive mode
Transient menu - Comprehensive command menu accessible via . or C-c .
Whats New
<2025-12-15 Mon> 0.3.0
Added transient menu interface
Introduced a comprehensive transient menu (dired-video-thumbnail-transient) providing quick access to all commands via . or C-c . in the thumbnail buffer. The menu displays current state (sort order, filters, video count, recursive/wrap mode) and organises commands into logical groups: Navigation, Playback, Sorting, Filtering, Marking, Delete, Display, and Other.
<2025-12-15 Mon> 0.2.0
Enhanced package with sorting, filtering, and docs
Added sorting and filtering features to dired-video-thumbnail. Introduced customizable options for sorting and filtering criteria, and implement interactive commands for toggling these settings. Included comprehensive documentation in Texinfo format, covering installation, usage, and customization.
Requirements
Emacs 28.1 or later
ffmpeg and ffprobe installed and available in your PATH
A new buffer opens displaying thumbnails for all videos in the directory
The cursor automatically moves to the first thumbnail
With Marked Files
In dired, mark specific video files with m
Run M-x dired-video-thumbnail
Only thumbnails for the marked videos are displayed
Recursive Mode
To include videos from subdirectories:
Use C-u M-x dired-video-thumbnail (with prefix argument)
Or run M-x dired-video-thumbnail-recursive
Or press R in the thumbnail buffer to toggle recursive mode
When dired-video-thumbnail-auto-recursive is enabled (the default), the package automatically searches subdirectories if the current directory contains no video files.
Press . or C-c . in the thumbnail buffer to open the transient menu. This provides a comprehensive interface to all commands with a live status display.
State: Sort: name ↑ | Videos: 42 | Recursive: OFF | Wrap: ON
Navigation Playback Sorting Filtering
n Next RET Play video s Sort menu... / Filter menu...
p Previous o Play video S Interactive sort \ Interactive filter
C-n Next row r Reverse order c Clear filters
C-p Previous row
d Go to dired
Marking Delete Display Other
m Mark menu... D Delete v Display menu... g Regenerate thumbnail
M Mark all x Delete marked+ Larger thumbnails G Regenerate all
U Unmark all - Smaller thumbnails C Clear cache
t Toggle all marks w Toggle wrap ? Help
R Toggle recursive q Quit menu
Q Quit buffer
The status line at the top shows:
Current sort criteria and direction (e.g., name ↑)
Number of videos displayed (and total if filtered)
Recursive mode status
Wrap display mode status
Active filters (if any)
Submenus
Several keys open submenus with additional options:
s - Sort menu: Sort by name, date, size, or duration; reverse order
/ - Filter menu: Filter by name regexp, duration range, or size range; clear filters
m - Mark menu: Mark/unmark current, toggle current, mark/unmark/toggle all
As you navigate between thumbnails, the header line dynamically displays information about the current video:
Mark indicator - A red * if the video is marked
Filename - The video filename in bold
Dimensions - Video resolution (e.g., 1920x1080)
Duration - Video length (e.g., 5:32 or 1:23:45)
File size - Size in MB (e.g., 45.2 MB)
The header also shows current sort settings (e.g., [name ↑]), active filters, and a [recursive] indicator when browsing subdirectories.
Keybindings
In the *Video Thumbnails* buffer:
Transient Menu
Key
Command
Description
.
dired-video-thumbnail-transient
Open transient menu
C-c .
dired-video-thumbnail-transient
Open transient menu
Navigation
Key
Command
Description
n
dired-video-thumbnail-next
Move to next thumbnail
p
dired-video-thumbnail-previous
Move to previous thumbnail
SPC
dired-video-thumbnail-play
Play video at point
C-f
dired-video-thumbnail-forward
Move to next thumbnail
C-b
dired-video-thumbnail-backward
Move to previous thumbnail
<right>
dired-video-thumbnail-forward
Move to next thumbnail
<left>
dired-video-thumbnail-backward
Move to previous thumbnail
<up>
dired-video-thumbnail-previous-row
Move up one row
<down>
dired-video-thumbnail-next-row
Move down one row
d
dired-video-thumbnail-goto-dired
Switch to associated dired buffer
q
quit-window
Close the thumbnail buffer
Q
dired-video-thumbnail-quit-and-kill
Quit and kill the buffer
Playback
Key
Command
Description
RET
dired-video-thumbnail-play
Play video at point
o
dired-video-thumbnail-play
Play video at point
mouse-1
dired-video-thumbnail-play
Play video (click)
On Linux, videos open with xdg-open. On macOS, they open with open. On Windows, they open with the system default player. You can also specify a custom player.
Marking
Marks are synchronised with the associated dired buffer, so marking a video in the thumbnail view also marks it in dired, and vice versa.
Key
Command
Description
m
dired-video-thumbnail-mark
Mark video and move to next
u
dired-video-thumbnail-unmark
Unmark video and move to next
mouse-3
dired-video-thumbnail-toggle-mark
Toggle mark (right-click)
M
dired-video-thumbnail-mark-all
Mark all videos
U
dired-video-thumbnail-unmark-all
Unmark all videos
t
dired-video-thumbnail-toggle-all-marks
Invert all marks
Deletion
Key
Command
Description
D
dired-video-thumbnail-delete
Delete video at point (with confirmation)
x
dired-video-thumbnail-delete-marked
Delete marked videos (with confirmation)
Display
Key
Command
Description
+
dired-video-thumbnail-increase-size
Increase thumbnail size
-
dired-video-thumbnail-decrease-size
Decrease thumbnail size
r
dired-video-thumbnail-refresh
Refresh the display
w
dired-video-thumbnail-toggle-wrap
Toggle wrap mode (flow vs fixed cols)
R
dired-video-thumbnail-toggle-recursive
Toggle recursive directory search
g
dired-video-thumbnail-regenerate
Regenerate thumbnail at point
G
dired-video-thumbnail-regenerate-all
Regenerate all thumbnails
Sorting
Key
Command
Description
S
dired-video-thumbnail-sort
Interactive sort menu
sn
dired-video-thumbnail-sort-by-name
Sort by filename
sd
dired-video-thumbnail-sort-by-date
Sort by modification date
ss
dired-video-thumbnail-sort-by-size
Sort by file size
sD
dired-video-thumbnail-sort-by-duration
Sort by video duration
sr
dired-video-thumbnail-sort-reverse
Reverse sort order
Filtering
Key
Command
Description
\
dired-video-thumbnail-filter
Interactive filter menu
/n
dired-video-thumbnail-filter-by-name
Filter by name regexp
/d
dired-video-thumbnail-filter-by-duration
Filter by duration range
/s
dired-video-thumbnail-filter-by-size
Filter by size range
/c
dired-video-thumbnail-filter-clear
Clear all filters
//
dired-video-thumbnail-filter-clear
Clear all filters
Help
Key
Command
Description
h
dired-video-thumbnail-help
Show help
?
dired-video-thumbnail-help
Show help
Customisation
All customisation options are in the dired-video-thumbnail group. Use M-x customize-group RET dired-video-thumbnail RET to browse them interactively.
Thumbnail Cache Location
Thumbnails are stored in ~/.emacs.d/dired-video-thumbnails/ by default:
Marked thumbnails are indicated with a coloured border. Customise the border width and colour:
(setq dired-video-thumbnail-mark-border-width 4) ;; Border width in pixels;; Change border colour via the face(set-face-foreground 'dired-video-thumbnail-mark"blue")
(setq dired-video-thumbnail-recursive nil) ;; Always search recursively(setq dired-video-thumbnail-auto-recursive t) ;; Auto-recursive when no local videos (default)
When dired-video-thumbnail-auto-recursive is enabled and the current directory has no video files but has subdirectories, the package automatically searches recursively.
Thumbnails are cached based on the file path and modification time. If you modify a video file, the thumbnail will be automatically regenerated on next view.
Video metadata (dimensions, duration) is also cached in memory to avoid repeated calls to ffprobe.
To manually clear the thumbnail cache:
M-x dired-video-thumbnail-clear-cache
Workflow Examples
Reviewing and Deleting Unwanted Videos
Open a directory with videos in dired
C-t v to open thumbnail view
Browse thumbnails with n, p, SPC, or arrow keys
Press D to delete individual videos, or mark with m and delete with x
Selecting Videos for Processing
Open thumbnail view with C-t v
Mark videos you want to process with m
Press d to switch to dired
Your marked videos are already selected in dired
Use any dired command (C, R, !, etc.) on marked files
Quick Video Preview
In dired, position cursor on a video file
C-t v opens thumbnail view
RET to play the video
q to return to dired
Finding Large Videos
Open thumbnail view with C-t v
Press . to open the transient menu
Press s then s to sort by size
Press r to reverse order (largest first)
Or use / then s to filter by size range
Finding Long Videos
Press . to open the transient menu
Press s then D to sort by duration
Or use / then d to filter by duration range (e.g., 5:00 to 30:00)
Searching by Name
Press . to open the transient menu
Press / then n and enter a regexp pattern
Only matching videos are shown
Press c to clear the filter
Troubleshooting
Thumbnails not generating
Ensure ffmpeg is installed: ffmpeg -version
Check that ffmpeg is in your PATH or set dired-video-thumbnail-ffmpeg-program
Try regenerating with g on a specific thumbnail
Placeholder showing instead of thumbnail
Some videos may fail to generate thumbnails if:
The video is corrupted
The timestamp is beyond the video duration (try setting dired-video-thumbnail-timestamp to nil)
ffmpeg doesn’t support the codec
Press g on the thumbnail to retry generation.
Video info not showing in header line
Ensure ffprobe is installed (it comes with ffmpeg). Set dired-video-thumbnail-ffprobe-program if it’s not in your PATH.
Marks not syncing with dired
Run M-x dired-video-thumbnail-debug to check if the dired buffer is properly associated. The output should show a live dired buffer reference.
Performance with many videos
The package processes up to 4 videos concurrently by default. For directories with hundreds of videos, initial thumbnail generation may take some time, but Emacs remains responsive and thumbnails appear as they complete.
Related Packages
image-dired - Built-in image thumbnail browser for dired
dirvish - A modern file manager for Emacs with preview support
So, I've seen some articles promoting Emacs-like editors written in Lisp
languages, and one of the most common arguments seems to be: "it's written in
This Lisp and also scriptable in This Lisp, and that gives it great
extensibility." 1
It's not wrong, but I think it does overlook a few things.
I had fun using the du command in Emacs this morning (that’s what I do when I don’t sleep well; judge away).
In Linux (and macOS), the DU command (I believe it stands for Disk Usage) is usually used to figure out what folders take up space on your hard drive.
While different GUI tools exist, they are not useful on encrypted drives on Linux, as they show the encrypted blocks instead of the directories, which don’t really tell you much (you could probably run those with escalated permissions, but I didn’t try).
For a gamer like me, it’s useful to see which games take up the most space. Here’s an example with a few useful flags, in the command line:
du -hs ~/.steam/steam/steamapps/common/* | sort -h
Some of the Steam-related components are not games, but… Helldivers 2 takes how much space?? Anyway.
A quick review of the options I used:
-hs for human readable (shows space in G for gigabyte and M for megabyte instead of writing in kilobytes) and summary (shows the top directory only)
~/.steam/steam/steamapps/common/* for the target path (this is where Steam stores the games in Linux, at least in Debian distros)
| to pipe the du command it into another command to read it better, in this case:
sort -h the sort command, which will sort it nicely again by order in human format. We need this last part if we want to see the directory in order, with the biggest one at the bottom.
Some places recommend using sort -hr, the additional r for reverse, which means in this case we will see the biggest directory at the top of the list. I don’t need it, because I want to see the biggest folder at the bottom, near the command line, which is where I’m going to focus next.
In Emacs, this command is easier use and works better thanks to Dired. Find a folder, mark it (m) in dired, and run dired-do-shell-command (!) and follow up with du -hs.
But since we’re in Emacs, and we might want to work with the results as text, we could use dired-do-async-shell-command (&). This will place the output in a temporary buffer we can work with (so we can save it to a text file, for example, with C-x C-w).
And here’s another thing I didn’t know: you can run these commands in Dired on multiple directories. Just mark several directories in Dired, and the resulting buffer will give you a list of all the directories you’ve marked. If you have this saved as a text buffer, it’s pretty easy to work withthe results (for example, save it as an org file and add headers telling you what you want to do with each directory).
By the way, even though it’s somewhat redundant with Dired’s default listing of files, you can also add the a option for du in this case ( for all) to display the files in the directories you’re viewing. This is useful in cases like the above, where you’re already working with the du command in Emacs and interested in looking at individual files as well, not just directories. Of course, you can just go in and list the files in Dired and open another Dired buffer with another directory listing files by size… this is Emacs, you have many ways to do whatever you want.
GNU Emacs has been my primary computing environment of choice for over
a decade. Emacs has enabled me to perform a wide array of tasks
involving human and computer languages, such as reading and writing
notes, emails, chats, programs, and more, all in a cohesive and
consistent environment that I can tailor exactly to my needs and
liking.
Coming from a Vim background, I started my Emacs journey trying some
configuration frameworks that provided vi-like key bindings, and after
a few Emacs bankruptcies, ended up with my current homegrown
configuration that I wrote from scratch gradually over the last
7 years, with inspiration from the configurations of some folks who
shared theirs publicly. Though my configuration has been mostly
stable for a few years now and I consciously keep the number of
external packages I use very small, I occasionally add small bits and
pieces to my configuration when I’m inspired after learning about a
neat feature or package on the blogs aggregated on Planet Emacslife,
the messages sent to the Emacs mailing lists, or the videos from the
annual EmacsConf conference.
I like getting a glimpse of other people’s worlds through the lens of
their creative works such as writings, be it prose or Emacs Lisp.
That’s only possible when people share freely, free as in freedom.
I’m thankful to Richard Stallman for his foresight to imbue GNU Emacs
with that freedom from the very beginning and for his lifelong fight
for computer user freedom, and to the many other folks who have joined
the free software movement since then and have fought the good fight.
I’ve been inspired and encouraged by many awesome Emacs people through
the years. People like Corwin Brust with his joyful creative energy
around Emacs and the road to software freedom, Sacha Chua and her
philosophy of leading a life of learning, sharing, and scaling, Gopar
and his enthusiasm for Emacs and its intersection with the Python
world, folks like Protesilaos Stavrou and Greg Farough who discovered
Emacs initially as non-programmers yet were enamoured by its
embodiment of software freedom in practice and went on to integrate it
into their everyday lives, and shoshin of the Cicadas cooperative at
the intersection of humanity and technology sharing his passion for
the human element and community by developing and contributing input
methods for his ancestral language of Lakota to GNU Emacs. I’m deeply
inspired by each of these wonderful people, and grateful for having
known them and for each of their unique perspectives and life stories
with which they have enriched my experience in Emacs and the free
software world.
As wonderful and impactful as Emacs has been in the lives of the many
who have come to know it throughout the decades that it’s been around,
it would not have become what it has been, what it is today, and what
it may become in the future without its community of passionate users
and contributors. The People of Emacs are all of us. Here’s to many
more of us, enjoying many more years of Emacs and software freedom
together even if spread far apart.
... and I am now, in terms of the tools that I am using to organize my life at the least, exactly where I have been a year ago.
Sure, I could now pretend that I have done a thorough yearly-reflection, got deep into myself and reflected about the year gone by, but simply pressing the "Random Note" button in Obsidian had been enough to stumble into the notes and journal entries that I took then.
A year ago I had been using:
Obsidian, for everything To Do, (Bullet-) journaling, note-taking, essentially for everything that involved typing words
Bearblog, for the blog (even though I then still had an instance of the blog on Micro.Blog and switched back and forth between them - or course I did)
In the time since then I used:
the Bullet Journal system in a Leuchttturm1917 book (both the "official" notebook and the normal ones)
the Bullet Journal system in the Traveler's Notebook with a variety of inlays to test which works best
the Bullet Journal system in the Hobonichi notebooks to "save time" as I don't have to set up any daily-/weekly-/monthly-layout
tried to settle on a journaling system in notebooks (Leuchtturm) or the Hobonichi 5 year one
Emacs mostly with Denote, but also tried to figure out to "optimize my workflows" in plain .txt files or even tried the .todotxt and .taskpaper thing
tried to do the .todotxt in a "random" text-editor (here Helix)
copied/migrated/whatever notes from either of the notebooks to either Obsidian, Denote, or between any of the notebooks/journals
...
Needless to say that all of the above involved (and in case my recent move back to Bear still involves) a STUPID AMOUNT OF TIME to migrate, to copy/paste and of course nothing is complete in terms of a continuing timeline.
It’s mostly a mess.
And I don’t want to even think about all the hassle that involved in finding the "right pen/ink/nib-combo" to write with in either of the notebooks.
And now, I am back where I have started, with essentially two applications: Obsidian and Bearblog!
This is a simple setup that does all that I need and that I can use anywhere I want (currently typing this on an Android tablet with an external keyboard, which feels rather comfortable). It goes without saying that I could have saved a stupid amount of time and money (notebooks and pens are really expensive) to have realized this already a year ago (or at any point in the years prior that) but that would have been too easy I guess.
Emacs is the most people-centric technology I’ve ever used. I’m willing to bet that’s true for others as well.
Who are your “People of Emacs”?
Apart from a brief University stint where we used Emacs to write some C code on our SUN terminals, my first year of Emacs was shaped by three people:
Sascha Fast, my pal, then-roommate and Zettelkasten partner in crime, who found out about Org Mode and just had to get me on that after a brief experimentation to manage tasks and projects in plain text. Sascha had to force me to even give it a try because I didn’t want to spend time offboarding OmniFocus for projects and TextMate for writing, and now look at me. Look. At. Me. I’m almost a radicalized GNU fanboy who considers switching the daily driver from Mac to Arch Linux.
Xah Lee for his Emacs Tutorial – I basically learned all my Emacs Lisp baby steps thanks to his hand-curated manual and reference with examples. I gave his modal input, Xah Fly Keys a try and realized what you could do with key bindings in this weird editor: the single key bindings to move by word or line, and to change the case of the word at point was a huge life-saver when proof-reading Sascha’s book draft. That got me hooked. I’m still on a old version of these bindings. Then I also got into mechanical split-ergo keyboards thanks to his website, upgrading from my Kinesis Freestyle. And during the COVID pandemic lockdown of 2020, I had a lot of fun on his Discord.
Sacha Chua for everything on her site including cool Emacs hacks, then Planet Emacslife and Emacs News as the community pulse that sucked me into the weirder things you could do with Emacs. I didn’t know about movie playback and email writing before that. Thanks for this, I guess, now I’m the “everything in Emacs” weirdo :)
Since then, there have been many more. Some through packages, some through writing.
Protesilaos Stavrou, whose modus-themes v1 won me over and kept me loyal thanks to the deuteranopia variants.
I, uh, ‘enjoy’ more than 200 use-package expressions in my init file … thanks everyone for sharing your work with the world, as free/libre software, and making Emacs better for all of us.
Hire me for freelance macOS/iOS work and consulting.
I spent some time figuring out how to submit a multipart/form-data form with url-retrieve-synchronously with Emacs Lisp. It was surprisingly hard to find an example of working with multi-part forms. I had totally forgotten that I had figured something out last year: Using Emacs Lisp to export TXT/EPUB/PDF from Org Mode to the Supernote via Browse and Access. Well, I still had to spend some extra time dealing with the quirks of the PeerTube REST API. For toobnix.org, having = in the boundary didn't seem to work. Also, since I had newlines (\n) in my data, I needed to replace all of them with \r\n, which I could do with encode-coding-string and the utf-8-dos coding system. So here's an example I can use for the future:
(let* ((boundary (format "%s%d" (make-string 20 ?-) (time-to-seconds)))
(url-request-method "PUT") ; or POST
(url-request-extra-headers
(append
(list
(cons "Content-Type"
(concat "multipart/form-data; boundary=" boundary))
;; put any authentication things you need here too, like;; (cons "Authorization" "Bearer ...")
)
url-request-extra-headers
nil))
(url-request-data
(mm-url-encode-multipart-form-data
`(("field1" .
,(encode-coding-string "Whatever\nyour value is"'utf-8-dos)))
boundary))
(url "http://127.0.0.1")) ; or whatever the URL is
(with-current-buffer (url-retrieve-synchronously url)
(prog1 (buffer-string)
(kill-buffer (current-buffer)))))
I've also added it to my local elisp-demos notes file (see the elisp-demos-user-files variable) so that helpful can display it when I use C-h f to describe mm-url-encode-multipart-form-data.
I just published the latest stable release of the Modus
themes. The change log
entry is reproduced further below. For any questions, you are welcome
to contact me. I will now work to
apply these same changes to emacs.git, so please wait a little longer
for the updates to trickle down to you.
Package name (GNU ELPA): modus-themes (also built into Emacs 28+)
Backronym: My Old Display Unexpectedly Sharpened … themes.
5.2.0 on 2025-12-31
This version fixes some bugs, adds a new feature for those who want to
derive a theme from Modus, and makes other small quality-of-life
refinements.
The modus-themes-with-colors should work at all times
In the transition to version 5.0.0, I inadvertently introduced
regressions to the behaviour of the modus-themes-with-colors macro.
This macro let binds the current theme’s palette around arbitrary
Elisp expressions, which allows users to access the named colours
therein. In versions 5.0.0 and 5.1.0 the macro could not read
variables defined outside its scope. Users needed to write an eval
around it, which I did not like. Now the macro should not require such
workarounds: it basically is a let that should work as expected
everywhere.
The modus-themes-generate-palette function to quickly get a palette
Users or package developers who want to create a theme on top of Modus
can now get a kickstart by defining their palette with the help of the
new modus-themes-generate-palette function. This function is meant
to return a complete palette, given a list of basic colours. Users can
thus experiment with their new theme while knowing that what they got
contains all the definitions; definitions that they may then modify
further (e.g. to define different semantic mappings than the defaults
such as, for example, to have (fg-heading-1 red-warmer) instead of
what originally is (fg-heading-1 fg-main)).
I have written extensive documentation in the manual, which includes a
complete example of a Solarized theme that is built on top of Modus.
If you have any questions, you are welcome to contact me.
Convenience commands to select only dark or light themes
The commands modus-themes-select-dark and modus-themes-select-light
use minibuffer completion to load a theme. The completion candidates
are filtered to only dark or light themes, respectively.
This is effectively the same as calling the command modus-themes-select
with a prefix argument (C-u by default).
Remember that we also have the commands modus-themes-load-random,
modus-themes-load-random-dark, and modus-themes-load-random-light.
Otherwise use the command modus-themes-rotate.
Improved prompt for theme selection
The minibuffer prompt used by the various Modus commands to select a
theme now has a grouping function in place: it shows the current theme
at the top and then all other themes grouped by their dark or light
background. This makes it easier to find a relevant theme, especially
if lots of them are present, such as when modus-themes-include-derivatives-mode
is enabled and relevant packages/themes are available (e.g. my
ef-themes and standard-themes).
Semantic colours for transient.el (e.g. in Magit)
The transient.el concept of “semantic colours” is now supported.
This is used by default in Magit to denote the different types of
keys, such as those that exit the transient, keep it active, move to
another transient, and the like. Users who prefer the old style where
all key bindings looked the same must customise the user option
transient-semantic-coloring.
Note that the deuteranopia- and tritanopia- optimised themes adapt
gracefully to such “semantics”, owning to relevant internal
refinements I made. Those themes cannot rely on the full colour
spectrum to communicate such nuances.
All hl-todo-mode faces use a bold weight if appropriate
When the user option modus-themes-bold-constructs is set to a
non-nil value, then all keywords that hl-todo-mode highlights will
be rendered in a bold weight (technically, they inherit the bold
face). This is how we were doing it before until I undid it by
mistake. Thanks to Dominik Schrempf for reporting the bug in issue
177: https://github.com/protesilaos/modus-themes/issues/177.
Theme-sensitive colours for Gnus mail groups
The Gnus mail groups no longer have hardcoded colour values. They will
look different depending on the current Modus theme.
Faces that set a :box attribute handle unspecified colours
I updated all faces that use a :box attribute to account for the
scenario of a user writing palette overrides that unset the relevant
colour. Thanks to JD Smith for reporting a bug along those lines in
issue 9 of my standard-themes repository (they are derived from the
modus-themes, hence the changes here):
https://github.com/protesilaos/standard-themes/issues/9.
The calendar-today and org-date-selected faces are disambiguated
These two faces are no longer using the same styles. This is because
they can appear in the same buffer. Thanks to Rudolf Adamkovič for
discussing this with me in the context of the same change for my
doric-themes (issue 20 in doric-themes.git):
https://github.com/protesilaos/doric-themes/issues/20.
The Modus “current theme” respects multiple enabled themes
The Modus concept of “current theme” respects the user’s choice for
multiple themes loaded at once. It will return the first Modus theme
even if it is not at the front of the list.
[ Emacs will load multiple themes by default, which leads to awkward
colour combinations, unless you know what you are doing—as such all
the Modus commands that load a theme will disable all others, subject
to the user option modus-themes-disable-other-themes. ]
Pierre has assigned copyright to the Free Software Foundation.
Fixed symbol of inherited AUCTeX face
There was a typo which caused an error. Thanks to Rudolf Adamkovič for
the patch and also for providing a relevant unit test. This was done
in pull request 188: https://github.com/protesilaos/modus-themes/pull/188.
Rudolf has assigned copyright to the Free Software Foundation.
Miscellaneous
Thanks to Basil L. Contovounesios for simplifying a couple of
expressions. This was done in pull request 190:
https://github.com/protesilaos/modus-themes/pull/190. Basil has
assigned copyright to the Free Software Foundation.
Several faces that had a strike-through effect when they did not
really need it are revised to use a wavy underline instead. The idea
is to let the text be readable at all times, regardless of the
effective font family. With the strike-through effect, some fonts
completely obscure the underlying text.Thanks to Morgan Willcock for
discussing with me the use of the strike-through style in issue 169:
https://github.com/protesilaos/modus-themes/issues/169.
All symbol-overlay faces are unique, fixing a mistake I had done
before.
The org-dispatcher-highlight, which is used to highlight the keys
of the Org export interface, now uses the appropriate foreground
colour and is always rendered in a bold weight.
The org-habit faces no longer call the function
readable-foreground-color. This is because that function does not
work if the theme is loaded via the early-init.el. Thanks to
Gaston Cabotin for reporting the problem in issue 174:
https://github.com/protesilaos/modus-themes/issues/174.
The gnus-button, which Gnus uses in all sorts of places to mark
some text as clickable, is styled with a less intense underline and
will no longer follow the style of links, including possible palette
overrides. This way, Gnus article buffers will not have visual
noise. Thanks to Morgan Willcock for discussing this with me in
issue 140: https://github.com/protesilaos/modus-themes/issues/140.
Now Valigo has his own video up that reaches the same conclusion. For him, the main attraction of Emacs is it’s extensibility and customizability. The reason for that, of course, is that the Emacs executable is basically a Lisp image with the source code available from within that image. That means that you can, if needed, reach into the guts of Emacs and change just about any aspect of Emacs on the fly. The only exception is the small C core and even that has the source code available from within Emacs but you’d have to recompile Emacs to change it.
One telling example that Valigo gives is to ask Emacs for the definition of the j key. Because he has evil mode enabled, Emacs reports that j runs the command evil-next-line. Then he disables evil mode and repeats the experiment. This time Emacs reports that j runs the self-insert-command to add a j to the buffer. The point is that the help command adapts itself on the fly to reflect the current state of the system.
Because of all this customizability, Emacs use is addictive. Once you start, you can’t stop. Like Tsoding, Valigo says that Emacs is old and crufty but he can’t escape because nothing else is as useful.
I get that Tsoding an Valigo are probably writing tongue in cheek but really, if Emacs is so useful you can’t live without it, why are you complaining? You can, after all, change anything you don’t like.
J'aime bien l'éditeur Emacs. C'est si personnalisable. Comme il est tellement personnalisable, quand on lit les fonctions, on peut avoir un aperçu de la vie des autres, de leurs objectifs, et des défis dont ils sont venus à bout avec les fonctions. En lisant le code, on découvre une petite partie de leur univers. Parfois on peut rencontrer des gens sur les blogs (l'agrégateur Planet Emacslife est très utile), les vidéos, les réunions virtuelles ou la conférence annuelle EmacsConf. J'aime particulièrement la série Prot Asks où il converse avec quelques personnes de tout et de rien. Les gens qui sont intéressés par Emacs sont toujours également intéressés par d'autres choses passionnantes. Même s'ils sont dispersés physiquement et sont occupés, ce qui fait que la coopération est très rare, j'apprécie qu'ils existent, ils créent, ils partagent…
Qui m'a le plus influencé ? C'est probablement John Wiegley. Son Planner Mode m'a aidée à organiser mes notes à l'université et m'a inspirée à l'utiliser et à faire du bénévolat. Son Ledger CLI m'a aidée à budgétiser, ce qui m'a permis cette expérience de la vie indépendante. Son idée pour Emacs News continue de me connecter à la communauté Emacs. J'ai pu le rencontrer en 2013 à l'EmacsConf à Londres. Quelle chance !
Beaucoup d'autres personnes me réchauffent le cœur. J'apprécie aussi Jon Snader pour toujours écrire beaucoup de commentaires sur son blog Irreal, et j'apprécie beaucoup de blogueurs, créateurs de vidéos, et ceux qui partagent des liens. J'apprécie kensanata qui entretient EmacsWiki, les modérateurs du salon #emacs et d'autres canaux d'IRC, et les bénévoles qui font de la modération sur les listes de diffusion. Ils font énormément de travail en coulisses pour rendre l'expérience plus plaisante pour nous. J'apprécie Eli Zaretskii et les autres mainteneurs d'Emacs, yantar92 qui entretient Org Mode, et les mainteneurs d'autres packages. Je suis toujours étonnée de voir que les gens partagent leur temps avec nous.
Bien sûr, il y a des difficultés. L'intelligence artificielle peut aider les gens à comprendre et à créer beaucoup de choses, mais ça peut aussi nous inonder de contenu insipide. Étrangement, certaines personnes ont du mal à être polies. Mais je ne dois pas leur laisser gâcher ma reconnaissance pour le reste. J'aime bien lire les billets de blog sur Emacs et les autres sujets, donc je dois ajouter plus de blogs à mon agrégateur.
Ensuite ? Je travaille lentement à copier les discussions d'EmacsConf. Je continue de publier le bulletin Emacs News. Un jour je veux enregistrer des vidéos et écrire des billets. Cette année semble plus difficile pour les gens : plus occupés, plus stressés… Peut-être qu'EmacsConf devrait aussi s'adapter aux temps changeants. Je me demande ce qui pourrait me faciliter la tâche. Cela me stresse surtout à cause de l'administration système, la conversion des vidéos et la gestion de plusieurs choses à la fois pendant la conférence, particulièrement en direct sur scène. Si je limite ça à une piste, elle sera peut-être plus gérable. Nous nous organisons ensemble.
Hourra pour les gens d'Emacs!
In English
I really like the Emacs editor. It's so customizable. Because it's so customizable, when you read functions, you can get a glimpse into other people's lives, their goals, and the challenges they've overcome with those functions. By reading the code, you discover a small part of their world. Sometimes you can meet people on blogs (the Planet Emacslife aggregator is very useful), videos, virtual meetings, or at the annual EmacsConf conference. I particularly like the Prot Asks series where he chats with various people about everything. People who are interested in Emacs are always also interested in other interesting things. Even if they're spread far apart and pretty busy, which makes collaboration very rare, I appreciate that they exist, they create, they share…
Who has influenced me the most? Probably John Wiegley. His Planner Mode helped me organize my notes in university, and it inspired me to use Emacs and volunteer. His Ledger CLI helped me budget, which allowed me to experiment with this independent life. His idea for Emacs News continues to connect me to the Emacs community. I got to meet him in 2013 at EmacsConf in London - how lucky!
Many other people warm the cockles of my heart. I appreciate Jon Snader for writing lots of comments on his Irreal blog. I'm grateful for bloggers, video creators, and people who share links. I appreciate kensanata for maintaining EmacsWiki, the moderators of the #emacs channel and other IRC channels, and the volunteers who moderate the mailing lists. They do a tremendous amount of work behind the scenes to make the experience more enjoyable for us. I appreciate Eli Zaretskii and other Emacs maintainers, yantar92 who maintains Org Mode, and the maintainers of other packages. I'm always amazed that people share their time with us.
Of course, there are challenges. Artificial intelligence can help people understand and create many things, but it can also flood us with slop. Some people struggle with politeness. But I shouldn't let that spoil my appreciation for everything else. I enjoy reading blog posts about Emacs and other topics, so I should probably add more blogs to my aggregator.
What's next? I'm slowly working on copying the EmacsConf discussions. I'll continue putting together Emacs News. Someday, I'd like to record more videos and write more blog posts. This year seems harder for people: busier, more stressed… Perhaps EmacsConf should also adapt to the changing times. I'm wondering what could make things easier for me. I get particularly stressed because of system administration, video conversion, and managing multiple things at once during the conference, particularly on stream. If I limit it to one track, it might be more manageable. We'll work it out together.
Hooray for the people of Emacs!
This was inspired by the Emacs Carnival theme for December, The People of Emacs. Thanks to George Jones for hosting!
(Thanks to bandali for helping me fix some typos!)
The steps presented here might work also for Ubuntu and later versions of Debian, but that’s only a wildly hopeful speculation that I cannot test.
Installing TeX Live 2025 on Debian 12
Set aside ninety minutes for this installation!
I successfully installed TeX Live 2025 on multiple machines running Debian 12 with the following steps.
Note that I closely followed the instructions from the Debian wiki and the TeX Live website on TUG.
I have added some clarifying remarks that might be helpful.
Uninstall the old .deb version of TeX Live
sudo apt autopurge texlive*
Just uninstalling TeXLive takes a couple minutes.
If you don’t remember when you installed these packages, wait until you see how many dependencies they have. TeX Live is the biggest package you’re likely to find installed on any Linux system. (No, I probably don’t need every module, but it has always been much easier to install the whole monolith than to deal with missing dependencies.)
cd /tmp
wget https://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz
tar xvf install-tl-unx.tar.gz
cd install-tl-2*
sudo perl ./install-tl --no-interaction
The instructions have a cute little note # may take several hours to run which prompted my LOL post to Mastodon. (Again, I choose to install everything here, 4,958 TeX Live packages. In fact the instructions presume a full install.)
It took about an hour on my newish desktop with Ethernet, almost two hours on my ten-year-old laptop over Wi-Fi.
Add the binary directory to your PATH
It will be of the form /usr/local/texlive/2025/bin/PLATFORM, likely /usr/local/texlive/2025/bin/x86_64-linux.
Be certain that root can run the TeX Live binaries too!
The way I did so was to edit /etc/profile, find the appropriate lines setting the path, and edit them to add that directory. The result looks like so:
if [ "$(id -u)" -eq 0 ]; thenPATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2025/bin/x86_64-linux"elsePATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/usr/local/texlive/2025/bin/x86_64-linux"fiexport PATH
Install a few minimal base packages via apt
sudo apt install tex-common texinfo lmodern
Configure a “dummy package” and install via apt
This part is opaque to me. It appears we are using something called equivs to wrap our raw-dog TeX Live install into a .deb in order to trick the package manager into playing nice with it, but I am innocent of the mechanism here.
We are now in a temporary directory that contains a configuration file for our dummy package called texlive-local.
Replace the contents of that file with the following:
Reality check: let’s make sure we can in fact use our new TeX Live install to produce a minimal accessible PDF. Note that the polyglossia package is a replacement for the babel package that adds accessibility features. But the \DocumentMetadata{} command does the heavy lifting for producing tagged PDFs. It must come before \documentclass{}.
\DocumentMetadata{
lang = en,
pdfstandard = ua-2,
pdfstandard = a-4f,
tagging = on
}
\documentclass{article}
\usepackage{polyglossia}
\setdefaultlanguage[variant=US]{english}
\begin{document}
\section{Lorem ipsum}
I know I promised US English but here's some nonsense
not-quite-Latin. Est eveniet accusamus dolor et. Possimus fugit
consectetur alias iure suscipit facere est exercitationem. Sed enim
sapiente atque.
Voluptas et tempora est. Recusandae velit qui nesciunt. Molestiae excepturi
occaecati doloribus. Eum sunt optio aut consequatur doloremque. Quo eveniet
rerum aut dicta impedit quia ut autem. Dolor nisi qui architecto sunt.
Corporis quidem aut natus est quidem pariatur. Error aut repellat nobis
velit corporis voluptatem. Libero hic nesciunt omnis ut quam minus soluta.
Ad quo culpa facere pariatur voluptas et quis nostrum. Pariatur tempore
ipsum voluptatibus iusto repudiandae. Earum ea quo saepe autem et. Eum
ratione eaque non dolorum ut fugit vitae dolorum.
\end{document}
Save this file as minimal.tex and run
lualatex minimal.tex
(On its first run, it will take a minute to build a font names database.)
If it doesn’t work, well, I guess you have some debugging to do?
Note that the LuaLaTeX engine is required for all the accessibilty new hotness. (TIL it has been the recommended engine for a year.)
Configuring Org LaTeX export to produce compliant PDFs
Add these expressions to your Emacs configuration.
Set the default engine to lualatex but allow individual files to override it (in case you have some weird old files, I suppose).
(setq org-latex-compiler "lualatex")
;;; %latex gets replaced with org-latex-compiler
;;; OR overridden by the #+LATEX_COMPILER header
(setq org-latex-pdf-process
'("latexmk -f -pdf -%latex -interaction=nonstopmode -shell-escape -output-directory=%o %f"))
Insert the \DocumentMetadata{} command before \documentclass{} for the article document type. Note that as written, only the article documentclass is defined for export. You might already have a org-latex-classes declaration in your config, in which case you should modify it to replace the article class definition with this one.
(defvarorg-latex-metadata"\\DocumentMetadata{lang = en, pdfversion = 2.0, pdfstandard = ua-2, pdfstandard = a-4}""LaTeX preamble command to specify PDF accessibility metadata.\nIt must appear before the \\documentclass{} declaration.")
(setq org-latex-classes
`(
("article" ,(concat org-latex-metadata "\n""\\documentclass[11pt]{article}")
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}"))
))
I have not yet attempted to make the corresponding definition for Beamer. When I get it to work I will update this post.
#+title: A nice title for a PDF
#+subtitle: a test of tagged PDF accessibility
#+latex_header: \usepackage{polyglossia}
#+latex_header: \setdefaultlanguage[variant=US]{english}
Est eveniet accusamus dolor et. Possimus fugit consectetur alias
iure suscipit facere est exercitationem. Sed enim sapiente atque.
* Heading 1
** Subhead 1.1
Voluptas et tempora est. Recusandae velit qui nesciunt. Molestiae
excepturi occaecati doloribus.
** Subhead 1.2
Eum sunt optio aut consequatur doloremque. Quo eveniet rerum aut
dicta impedit quia ut autem. Dolor nisi qui architecto sunt.
* Heading 2
** Subhead 2.1
Corporis quidem aut natus est quidem pariatur. Error aut repellat
nobis velit corporis voluptatem. Libero hic nesciunt omnis ut quam
minus soluta. Ad quo culpa facere pariatur voluptas et quis nostrum.
** Subhead 2.2
Pariatur tempore ipsum voluptatibus iusto repudiandae. Earum ea quo
saepe autem et. Eum ratione eaque non dolorum ut fugit vitae
dolorum.
Dealing with remaining issues
It’s important to remember that the accessibility functionality in LaTeX is still new and incomplete. But it’s a big step from zero to very good.
For me and everyone who posts our course materials to Canvas, the touchstone is the “Ally Accessibility Checker,” which is also not perfect. For instance, it insists that PDFs produced by LaTeX are missing a title when they are not. If I can get to the bottom of that bug, I’ll update this post.
In 2024, I took the leap to go indie full-time. By 2025, that shift enabled me to focus exclusively on building tools I care about, from a blogging platform, iOS apps, and macOS utilities, to Emacs packages. It also gave me the space to write regularly, covering topics like Emacs tips, development tutorials for macOS and iOS, a few cooking detours, and even launching a new YouTube channel.
The rest of this post walks through some of the highlights from 2025. If you’ve found my work useful, please consider sponsoring.
Off we go…
Launched a new blogging service
For well over a decade, my blogging setup consisted of a handful of Elisp functions cobbled together over the years. While they did the job just fine, I couldn't shake the feeling that I could do better, and maybe even offer a blogging platform without the yucky bits of the modern web. At the beginning of the year, I launched LMNO.lol. Today, my xenodium.com blog proudly runs on LMNO.lol.
LMNO.lol blogs render pretty much anywhere (Emacs and terminals included, of course).
2026 is a great year to start a blog! Custom domains totally welcome.
A journaling/note-taking app that feels like tweeting
Sure, there are plenty of journaling and note-taking apps out there. For one reason or another, none of them stuck for me (including my own apps). That is, until I learned a thing or two from social media.
With that in mind, Journelly was born: like tweeting, but for your eyes only. With the right user experience, I felt compelled to write things down all the time. Saving to Markdown and Org markup was the mighty sweet cherry on the cake.
Let's learn Japanese
As a Japanese language learning noob, what better way to procrastinate than by building yet another Kana-practicing iOS app? Turns out, it kinda did the job.
2025 brought us the likes of Claude Code, Gemini CLI, Goose, Codex, and many more AI/LLM CLI agents. While CLI utilities have their appeal, I wanted a native Emacs integration, so I simply ignored agents for quite some time.
I was initially tempted to write my own Emacs agent, but ultimately decided against it. My hope was that agent providers would somehow converge to offer editor integration, so I could focus on building an Emacs integration while leveraging the solid work from many teams producing agents. With LLM APIs historically fragmented, my hope for agent convergence seemed fairly far-fetched.
I'm fairly happy with how agent-shell's been shaping up. This is my most popular package from 2025, receiving lots of user feedback. If you're curious about the feature-set, I've written about agent-shell's progress from early on:
While most of what I share usually ends up as a blog post, this year I decided to try something new. I started the Bending Emacs YouTube channel and posted 8 episodes:
While migrating workflows to Emacs makes them extra portable across platforms, I've also accumulated a bunch of tweaks enhancing your Emacs experience on macOS.
EverTime for macOS
While we're talking macOS, I typically like my desktop free from distractions, which includes hiding the status bar.
Having said that, I don't want to lose track of time, and for that, I built EverTime, an ever-present floating clock (available via Homebrew).
A new time zone Emacs package
Emacs ships with a perfectly functional world clock, available via M-x world-clock, but I wanted a little more, so I built time-zones.
For better or worse, I rely on WhatsApp Messenger. Migrating to a different client or protocol just isn't viable for me, so I did the next best thing and built wasabi, an Emacs client ;)
While both macOS and iOS offer APIs for generating URL previews, they also let you fetch rich page metadata. I built rinku, a tiny command-line utility, and showed how to wire it all up via eshell for a nifty shell experience.
With similar eshell magic, you can also get a neat cat experience.
At one with your code
I always liked the idea of generating some sort of art or graphics from a code base, so I built one, a utility to transform images into character art using text from your codebase. Also covered in a short blog post.
Emacs can trim your videos too
Emacs is just about the perfect porcelain for command-line utilities. With little ceremony, you can integrate almost any CLI tool. Magit remains the gold standard for CLI integration.
While trimming videos doesn't typically spring to mind as an Emacs use case, I was pleasantly surprised by the possibilities.
Some time ago, Protesilaos Stavrou published a nice book on Emacs Lisp. The idea is to bring a “big picture approach” to Elisp so that every Emacs user can experience the joy of fine tuning Emacs to meet their exact needs.
Just recently, Stavrou has added EPUB and PDF versions. In a way, it doesn’t matter since he provides the Org mode source and you can export that to almost any format you want. Now, though, he has nice PDF and EPUB versions that you can simply download and read in your preferred format.
It’s nice having the book available as, for example, an Info file but apparently I’m old fashioned and prefer to read it as a PDF. Others may like EPUB or Info. Whatever your preferred format, Stavrou has you covered.
There aren’t that many books addressing Elisp and how to use it. Marcin Borkowski’s Hacking your way around in Emacs is one good example and there are some short tutorials but Stavrou’s and Borkowski’s books are the only ones I can think of off hand that address Elisp exclusively.
I’ve skimmed through the book and it seems like a good introduction. If you’re an Emacs user and want to advance, you really should learn a bit of Elisp. It’s not as daunting as it might seem since even adjusting the Emacs configuration is an exercise in using Elisp.
“I cannot understand every leaf of a tree. There is just too much. But if I understand the essence of the tree, I will intuitively know what will come out and when.”
By understanding the root, the understanding of the branches arises naturally. And understanding the rest follows.
What is the root but the planted seed? The one source of truth where everything else came from.
Studying all the patterns within all leaves of a tree is a tremendous task. But one might understand more about the tree just from its seed.
Everything is connected
Often, what appears to be unrelated is but the prolongation of the same thing.
Everything seems separated—not because they objectively are, but because we are unable to see the interconnection yet.
Every aha moment is the finding of that one dot connecting two seemingly isolated points.
I keep noticing this. A real-life situation that can be resolved using a paradigm from programming. A problem in one area that mirrors a pattern from somewhere else entirely. It happens so often that I have started to expect it.
Most innovation in the world is not novelty. It is borrowed concepts from other domains. A combination of what appeared to be unrelated.
I was watching a video recently—Michael Levin on Lex Fridman’s channel—talking about how different domains seem to point to the same direction about the manifestation of “emergence.” It struck me. Isn’t that what mystic figures were talking about thousands of years ago? Scriptures from different religions already pointing to the same thing?
Different waters, same fountain.
Finding the fountain is what understanding means to me.
What is timeless must be true
What is true in one place must be true in another. What is timeless must be true. And what is fundamentally true must remain true across different domains.
This is why I am drawn to philosophy, psychology, spirituality. This interest in the meta—in understanding itself.
And somehow it led me to Clojure, LISP, Emacs, Smalltalk. To the philosophy behind FOSS. Not just because of how they work, but because of what they represent—meta-programming, self-modification, the freedom to understand all the way down.
What is even greater is that these tools attract like-minded people.
When I read articles from the LISP or Emacs community, I see it. They are not just tinkering with tools. They are building deep understanding—a meta-understanding beyond their craft.
Consciously or not, we are pursuing the same Dao, if I might say it differently. Seeking essential patterns of life.
I wonder how many fountains there are. Or if it has always been just one.
I’m a long time user of Ctrl+x8Return for inserting arbitrary Unicode characters. I use it, for instance to insert the red meat character () for my Red Meat Friday posts.
For mundane accents like é, I use ivy-insert-org-entity that I stole from John Kitchin’s Scimax. It’s lighter weight and easier to use for spelling peoples’ names or using the occasional foreign accented word.
Even though I’ve been using Ctrl+x8Return for many years, I had no idea of how powerful the command is until I read this post from Rahul M. Juliato. It turns out that in addition to just using an existing Unicode character you can compose accented characters manually, combine multiple code points into a single glyph, and much more.
Juliato’s post is long and complicated so I won’t try summarize it here. You really need to take a look at the post itself. I had difficulty making some of his examples work but that was probably me doing something wrong or a shortcoming of my fonts. If you’re like me, you’ll probably never need all the power he describes but it’s nice to know it’s there if you do need it.
Any serious Emacs user is apt to have a lot of minor modes active at any given time. That’s certainly true in my case. What we don’t need, though, is to see lighters for each and every one of them. It wouldn’t matter so much if they didn’t fill up the mode line and obscure other indicators that you might actually want to see.
The current way of dealing with this is Diminish that prevents any of the minor modes listed from putting their lighters on the mode line. I’ve found them difficult to get configured correctly but they do the job and I haven’t thought about the problem in years.
In an
earlier post
I talked about making the hover signatures for
lsp-mode
a little bit more useful.
Out of the completely irrational desire to use more built-in packages,
I’ve recently switched to
eglot
as my LSP client of choice,
which however has the same inclination for showing hover information I don’t care about by default.
I won’t assume that you have read the lsp-mode post,
so let’s quickly remind ourselves of the general problem:
Lsp clients have the option of showing useful things on hover.
In most languages, there is an obvious candidate for this: the type signature of the thing at point.
Sadly—for some languages—the implementation of the feature is… not great.
This still holds true two years later,1
but this is as good of an excuse as any to become a bit more familiar with the eglot codebase.
Plus, customising Emacs is just so much fun.
Different language servers behave a bit differently here,
but the default for all of them is pretty “meh”.2
rust-analyzer shows where the thing comes from
I will draw your attention to the second line in the minibuffer;
the first is just additional context,3
and the third are available code actions.
What eglot shows me is that apply_ctx is a method of the Type type,
sitting in ./type/context.rs,
with ./ being the root of the crate.
clangd shows the vague type
(based)pyright shows something completely useless
ocaml-lsp sort of shows what I want to see
…but only for short type signatures
Same issue with haskell-language-server
What I’d like to achieve instead is a type signature—if I want to know where the thing comes from I can just M-. it.
In case anyone finds this useful,
I’ve packaged the source code of this article as eglot-hover.
It may need some adjustments depending on the language server implementation,
but at least for the ones shown above it should work as-is.
Onto the fun part.
The good—or bad, depending on your inclination—thing about lsp-mode is that it’s almost unnecessarily configurable just by user options.
This includes hover signatures:
lsp-clients-extract-signature-on-hover is just a cl-defmethod,
which one can trivially override depending on the name of the currently active language server (one of its arguments).
Eglot doesn’t actually do any displaying itself,
but instead delegates to the built-in ElDoc.
Scrolling through eglot’s code, we can find integration functions like this:
(defun eglot-hover-eldoc-function (cb &rest _ignored)
"A member of `eldoc-documentation-functions', for hover."
(when (eglot-server-capable :hoverProvider)
(let ((buf (current-buffer)))
(eglot--async-request
(eglot--current-server-or-lose)
:textDocument/hover (eglot--TextDocumentPositionParams)
:success-fn
(eglot--lambda ((Hover) contents range)
(eglot--when-buffer-window buf
(let* ((info (unless (seq-empty-p contents)
(eglot--hover-info contents range)))
(pos (and info (string-match "\n" info))))
(while (and pos (get-text-property pos 'invisible info))
(setq pos (string-match "\n" info (1+ pos))))
(funcall cb info :echo pos))))
:hint :textDocument/hover))
t))
How exactly to handle the callback cb is documented in eldoc-documentation-functions,
but that’s actually not super important for this application.
We don’t want to add anything to eglot’s already generated hover signature,
but completely replace it in certain contexts,
which essentially boils down to redefining pos.
A priori, this is a buffer position up until which the docstring will be shown;
as you can see above, the default implementation is to just show the first “real” line.
While pos is named as if it should always be a number,
it gets passed into the callback as a value for the :echo key.
Quoting the eldoc-documentation-functions documentation:
:echo, controlling how eldoc-display-in-echo-area should
present this documentation item in the echo area, to save
space. If VALUE is a string, echo it instead of DOCSTRING. If
a number, only echo DOCSTRING up to that character position.
If skip, don’t echo DOCSTRING at all.
The value can be a string, in which case that string is displayed verbatim.
Hence, the only thing we need to do is to monkey patcheglot-hover-eldoc-function,
match on the name of the current major mode,
and extract the “correct” signature ourselves if needed.
Suppose we are given the magic functions
eglot-hover--get to extract the signature out of the response that the LSP server sends,
as well as eglot-hover--hl-string to highlight the resulting string.
Then, matching just on rustic-mode,
the final change is rather small:
Since we’re not returning a buffer position, we have a lot more freedom to highlight the string as we want.
For example, in haskell-mode I prettify some symbols (forall becomes ∀, \ becomes λ, and so on),
and these would otherwise get lost when just the buffer position is sent to ElDoc.
The actual implementation of eglot-hover--hl-string and eglot-hover--get is relatively straightforward,
and just involves some markdown mangling.
It originally started with some Rust-specific code given in
emacs-lsp/lsp-mode#1740,
and was then generalised to fit other LSP servers.
You can try throwing servers not listed above at it as well,
but since the implementation is relatively brittle it’ll probably need some adjustments.4
To be clear, I don’t think these are shortcomings of the individual language servers,
it’s just that there’s a mismatch with the integration into Emacs’s LSP client landscape.
Breaking long things into several lines is quite normal, after all.↩︎
I do like eglot’s default behaviour of showing that we’re inside of a function call and
which argument is currently being filled in!↩︎
Needs at least cl-lib.el, dash.el, and s.el to work.↩︎
Most Emacs users run a tone of minor modes and many of them contribute something
(usually useless) to the modeline. The problem is that the modeline is not
infinite and can quickly get quite cluttered. That’s why for the longest time
I’ve been using the third-party diminish package and I have something like
this in my config:
diminish gets the job done, but it’s a bit annoying that you need a
third-party package for something so basic. Fortunately that’s about to
change…
I just learned that in Emacs 31 it’s finally possible to hide minor modes in
the modeline using built-in functionality! Here’s how you can do the
above:
And here’s how you can hide all minor modes (probably a bad idea, though, as
some add useful info to the modeline):
(setqmode-line-collapse-minor-modes'(not))
For more info on what you can do with this new functionality see C-h v mode-line-collapse-minor-modes.
After all, they don’t call Emacs the “self-documenting editor” for no reason.
From the docs you’ll learn that hidden mode “lighters” (Emacs lingo for a mode’s modeline indicator)
get compressed into one. It’s ... by default, but it can be customized via
the variable mode-line-collapse-minor-modes-to.
Apart from diminish, there are also the newer delight and
minions packages that tackle more or less the same problem.
As explained here
for minions, they might still be useful, depending on your use-cases.
One of the great aspects of Emacs is having options and when it comes
to dealing with the minor mode lighters we have plenty of options!
That’s all I have for you today. Happy Christmas holidays! Keep hacking!
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!