Sacha Chua: Create a Google Calendar event from an Org Mode timestamp

Time zones are hard, so I let calendaring systems take care of the conversion and confirmation. I've been using Google Calendar because it synchronizes with my phone and people know what to do with the event invite. Org Mode has iCalendar export, but I sometimes have a hard time getting .ics files into Google Calendar on my laptop, so I might as well just create the calendar entry in Google Calendar directly. Well. Emacs is a lot more fun than Google Calendar, so I'd rather create the calendar entry from Emacs and put it into Google Calendar.

This function lets me start from a timestamp like [2026-04-24 Fri 10:30] (inserted with C-u C-c C-!, or org-timestamp-inactive) and create an event based on a template.

(defvar sacha-time-zone "America/Toronto" "Full name of time zone.")

;;;###autoload
(defun sacha-emacs-chat-schedule (&optional time)
  "Create a Google Calendar invite based on TIME or the Org timestamp at point."
  (interactive (list (sacha-org-time-at-point)))
  (browse-url
   (format
    "https://calendar.google.com/calendar/render?action=TEMPLATE&text=%s&details=%s&dates=%s&ctz=%s"
    (url-hexify-string sacha-emacs-chat-title)
    (url-hexify-string sacha-emacs-chat-description)
    (format-time-string
     "%Y%m%dT%H%M%S" time)
    sacha-time-zone)))

(defvar sacha-emacs-chat-title "Emacs Chat" "Title of calendar entry.")
(defvar sacha-emacs-chat-description
  "All right, let's try this! =) See the calendar invite for the Google Meet link.

Objective: Share cool stuff about Emacs workflows that's not obvious from reading configs, and have fun chatting about Emacs

Some ideas for things to talk about:
- Which keyboard shortcuts or combinations of functions work really well for you?
- What's something you love about your setup?
- What are you looking forward to tweaking next?

Let me know if you want to do it on stream (more people can ask questions) or off stream (we can clean up the video in case there are hiccups). Also, please feel free to send me links to things you'd like me to read ahead of time, like your config!"
  "Description.")

It uses this function to convert the timestamp at point:

sacha-org-time-at-point: Return Emacs time object for timestamp at point.
(defun sacha-org-time-at-point ()
  "Return Emacs time object for timestamp at point."
  (org-timestamp-to-time (org-timestamp-from-string (org-element-property :raw-value (org-element-context)))))

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

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

-1:-- Create a Google Calendar event from an Org Mode timestamp (Post Sacha Chua)--L0--C0--2026-04-17T13:26:41.000Z

Michal Sapka: Updates Q1/2026

Let's try something new - quarterly update. I found great joy from reading ones from マリウス , so why not? I don't want it to be a week-note type of list, as prose is from humans and lists are from machines.

I want such updates (name may be subject to change) where I mind dump things, which never grew into full posts. So, instead of a 5 sentence post, they will be 5 sentences in a combined post.

Personal

Health

What I've been up to this year? Well, mostly I've been sick. The Kid is old enough to be sick less often, but when he does, he brings the best viruses with him. I wanted to write this update a few weeks ago, but well. I'd rather be healthy than published.

Speaking of The Kid. We are continuing our Montessori education, as we accepted to such school. Let's just hope he won't grow to be a Musk of something.

But, returning to health: since I'm an old, sickly person with a high cholesterol level, I needed to return to eating healthier. No more cakes on the go, no more sushi rice. I have, however, rediscovered a love from a few years back: natto. A friend showed it to me and it was superb. I now order it and eat it a few times a week. It tastes as good as it looks!

Phone and reading

My love towards the Hibreak is only growing stronger. I find no downsides of not being in Apple/Google duopoly. Yes, it's an android, but I'm using only FOSS applications. My random usage of social media on the go went down to zero. No Mastodon, no Google YouTube. It's a purposeful device: I can use it as a communicator, and I can use an e-book reader. The latter is going extremely strong! It took a while, but now I pick a book for a page or two just waiting in line. Reading became just a regular thing I do though the day! It's less taxing than looking at TechCrunch, but it's much more stimulating.

Books are a good idea. Who would have thought?

As for future plans: I was going to pick up Dune and FINALLY read the entire saga, but this has to wait. I got into possession of In Search of Lost Time , which I aim to dig into. I'll mix it up with Dumas, so I'm in my Emily in Paris phase. Just less dumb... I think. I haven't seen a single episode. Ergo: then plan: is Dumas -> something small -> Proust -> Something small and back to Dumas.

I also take much fewer photos. Not having a good camera on me all the time is a nice thing. I picked up my old, trusty Fuji X-100S as you may have noticed on how bad the photos look in recent posts. I need to finally learn to measure light...

Random other things

I rebuilt my pantsu-collection with a few Wrangler Frontiers. They are the best fitting jeans I've ever used, and now I own 6 pairs. No random hole will a problem and one of them is now my house pantalons. Screw sweatpants.

I also returned the PS5. It was an eventful period where I became disgusting gamer. The games were nice, but now I'm back at PS4 and I fail to see as a significant downgrade. We have peaked long time ago. I strongly prefer to use PS4.

Computer stuff

Thinkcentre

I replaced my Lenovo Thinkpad with an Lenovo Thinkcentre. I don't leave the house, so I don't need a laptop. MiniPC is very small and fits the desk nicely.

But, most of all, it's an all AMD system. This makes rocking FreeBSD a pleasure! No more breaking things due to Nvidia driver incompatibility. Things just work.

Lathe

In between being ill, I rewrote this page. The old version had posts written in plain old HTML. Some post-processing (like images) was necessary, so I put myself into regexp hell. Not that those were big regexp, but they are big enough for not to want to update them ever.

This means I needed something in between me and the HTML. Markdown was a no-go, as I hate it. It's good for small notes, but anything bigger? Nah. The answer was clear: LISP. Who wouldn't want to write in LISP? And so I wrote a LISP-like processor in the old python-based generator. It worked, it was fun to write in, but it's also terrible. I had no idea how to parse lisp, so I made it something with parens. The POC was there, just the implementation needed to entirely change - it was a a great example in how to not to do LISP.

And so I am writing a small Lisp parser now. I'm not aiming at being full common-lisp compatible, but still I try the API to be as correct as possible. Now, this will not be real LISP: I use arrays under the hood, not CONS. But all defuns, setqs, and so are already working. This is my first project on Go and I have to say that I love it. It's a modern language and environment, so writing in it a lot of fun... unlike some other languages, but more on them later on.

The project I call Lathe will be open sourced in the coming weeks. This site will also be fully migrated over coming months, but it will require some translators. I am able to write then in Lisp now, so it will be fun.

The biggest missing element of the puzzle is Macro support, but that's not needed for first release.

All this comes with a huge asterisk: I have no idea how to write Lisp. I am not a Lisp developer, and I am learning as I go. Here, it's the cherry on top. I like what I'm writing, I like that I'm learning, and I like how I'm writing. Go is now my friend.

The things some people will do just to not have to deal with Markdown nor Hugo.

Masto-mailo-inator

I got my first feature request! I am officially an open-source developer. And by that, I mean: unpaid.

I plan to add import from export next month, as currently I am fully focused on Lathe

GPG

I also started using GPG again. You can find my key on keys.opengpg.com

Work

Well, I'm still employed, which is great. It's over a decade in the same company! However, there are two things which changed in the first quarter

GenAI

I am not hiding this, but let's make it official: I use LLMs at work. Not because they make me more productive, nor because they make me happy. There is one reason: I am expected to. It's a sign a great technology where most people either reject or all together or are forced to use.

My team was moved to a different product, which is written in Java. I already miss Ruby.... They say that in the (age of ai) you don't need to know what you are doing, but I disagree with it on all fronts. I see it in my own experience. While, yes: I am able to generate hundreds lines of code, but I find to be terrible.

I always tried to understand what I'm doing, and I was even praised for it. Using Claude makes it extremely difficult. It's a new language, new framework, and yet we are expected to ship features within the first couple of weeks. Some teams are proud of skipping the standard few-month-long rump up. I think they are managed by dangerous morons. The code is still essential - it needs to work, it needs to it a reasonable fashion, and it needs to be readable. Whatever vibe coders say about prompting the next google, they are lying to themselves. Opus 4.6 is the best coding model out there (as I've read on multiple occasions) , and it still requires anal level of hand holding. While mostly everything it creates is a more-or-less correct java code, it's rarely good java code nor a properly designed system. It makes random changes, makes incorrect assumptions, just plain lies.

To give an example. We are integrating this service with another service. I wrote code which worked on localhost, but not on server. I try, debug, use curls - nothing. Finally, a few sessions laster I learn that it never worked. I didn't double-check the local curl, and I trusted Claude when it said that everything is working. This was a learning lesson, and I will never trust a clanker again. It will lie, to make me happy. Even if means not doing its job. Lessn learned: never, ever trust a clanker.

And the debugging, oh my god the debugging. It reads a million files, runs tests, does magical things - and boom, solution. So I ask a basic question (what about...?) . Of course, you are right - the moron replies, let's burn yet another 20 USD (LLM is a short for LLM Like Money) . It can go for like this few dozen prompts, back and foth, and still sometimes it will return to incorrect assumption from half an hour before. It will ignore requests, specs, do random things. It's far from an intern...

The fact stands: it's a better java developer than I am. But I am a terrible java developer. I never wrote any line of Java before! I have no idea how it will play out, as I see it with all my colleagues (and, most likely, the entire industry) have no idea what we are doing. Something looks like it's working, and we are expected to ship it. Not that there is any hard requirement, but it's a race. Layoffs are a regular thing now, and it's a dumb idea to be on the naughty list. I'd not use any SASS in the comming years, as I trust them even less than I used to.

Now, I have a great manager who understands that understanding is essential. I am able to slow down, and learn - little by little and with obvious expectatins of stil shipping stuff. But I am lucky to have him, and who knows for how long.

Java

The other thing: I am now a Java developer. Oh, what a terrible life it is. The language is... OK, at best. Nonetheless, is extremely stagnated. The developer experience is abysmal!

I have a working theory that IntelliJ is the worst thing which happened to Java folks. They have zero insensitive to fix things, or work on adding modern things. There is CLI, but it's a pita to work on. There is an LSP, but it's barely working. Both are under-invested, as IntelliJ is there, keeping the entire ecosystem in its dark ages.

I try to use Emacs, and with the genai is almost nice. More on this later. But, I understand why people use this godforsaken IDE: people use IntelliJ because other people use IntelliJ. It fixes a million things which should not be fixed in an editor, but in the ecosystem. Toying with (Go) at the same time just shows how primitive Java is.

And there is Spring . If anyone comes to me and whines about how much magic is in Rails , I will point them here. This and Lombok are much bigger obstacle to learn to read code than the language itself.

It's better to have experience in Java than not to have. We are living in the age of layoffs, after all. But it's a miserable life.

RTO

And, starting next month, I am expected to be twice a week in the office. I don't have a long commute (15 mins?) , and I'll be able to drop The Kid at school on the way. This changes nothing: the idea of office should be left in the past.

Emacs

My beloved editor deserves a special method. Since I'm again actively coding (after hours mostly) , I finally set up LSP, consult and all that jazz. At work, I'm rocking Agent Shell. And Ready Player One became my music player, and I moved to using mu4e for my email needs.

I also finally managed to get X11 forwarding over SSH working. Therefore, I get my private Emacs (with emails, rsses, mastodons) at my work Macbook. This will be a short guide in the coming weeks, but it's working over the local area. We'll see if RTO won't make it more challenging...

-1:-- Updates Q1/2026 (Post Michal Sapka)--L0--C0--2026-04-17T11:11:07.000Z

Dave Pearson: blogmore.el v4.1

Following on from yesterday's experiment with webp I got to thinking that it might be handy to add a wee command to blogmore.el that can quickly swap an image's extension from whatever it is to webp.

So v4.1 has happened. The new command is simple enough, called blogmore-webpify-image-at-point; it just looks to see if there's a Markdown image on the current line and, if there is, replaces the file's extension with webp no matter what it was before.

If/when I decide to convert all the png files in the blog to webp I'll obviously use something very batch-oriented, but for now I'm still experimenting, so going back and quickly changing the odd image here and there is a nicely cautious approach.

I have, of course, added the command to the transient menu that is brought up by the blogmore command.

One other small change in v4.1 is that a newly created post is saved right away. This doesn't make a huge difference, but it does mean I start out with a saved post that will be seen by BlogMore when generating the site.

-1:-- blogmore.el v4.1 (Post Dave Pearson)--L0--C0--2026-04-17T09:25:37.000Z

Sacha Chua: Make chapter markers and video time hyperlinks easier to note while I livestream

I want to make it easier to add chapter markers to my YouTube video descriptions and hyperlinks to specific times in videos in my blog posts.

Capture timestamps

Using wall-clock time via Org Mode timestamps like makes more sense to me than using video offsets because they're independent of any editing I might do.

C-u C-c C-! (org-timestamp-inactive) creates a timestamp with a time. I probably do often enough that I should create a Yasnippet for it:

# -*- mode: snippet -*-
# name: insert time
# key: zt
# --
`(format-time-string "[%Y-%m-%d %a %H:%M]")`

I also have Org capture templates, like this:

(with-eval-after-load 'org-capture
  (add-to-list
   'org-capture-templates
   `("l" "Timestamp" item
     (file+headline ,sacha-stream-inbox-file "Timestamps")
     "- %U %i%?")))

I've been experimenting with a custom Org Mode link type "stream:" which:

  • displays the text in a larger font with a QR code for easier copying
  • sends the text to the YouTube chat via socialstream.ninja
  • adds a timestamped note using the org-capture template above

Here is an example of that link in action. It's the (Log) link that I clicked on.

Let's extract that clip
(compile-media-sync
 '((combined (:source
              "/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs.mp4"
              :original-start-ms "51:09"
              :original-stop-ms "51:16"))
   (combined (:source
              "/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs-link-overlay.png"
              :output-start-ms "0:03"
              :output-stop-ms "0:04"))
   (combined (:source
              "/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs-qr-chat-overlay.png"
              :output-start-ms "0:05"
              :output-stop-ms "0:06")))
 "/home/sacha/proj/yay-emacs/ye16.1-stream-show-string-and-calculate-offset.mp4")

I used it in YE16: Sacha and Prot talk Emacs. It was handy to have a link that I could click on instead of trying to remember a keyboard shortcut and type text. For example, these are the timestamps that were filed under org-capture:

  • Getting more out of livestreams
  • Announcing livestreams
  • Processing the recordings
  • Non-packaged code

Here's a short function for getting those times:

(defun sacha-org-time-at-point ()
  "Return Emacs time object for timestamp at point."
  (org-timestamp-to-time (org-timestamp-from-string (org-element-property :raw-value (org-element-context)))))

Next, I wanted to turn those timestamps into a hh:mm:ss offset into the streamed video.

Calculate an Org timestamp's offset into a YouTube stream

I post my YouTube videos under a brand account so that just in case I lose access to my main sacha@sachachua.com Google account, I still have access via my @gmail.com account. To enable YouTube API access to my channel, I needed to get my brand account's email address and set it up as a test user.

  1. Go to https://myaccount.google.com/brandaccounts.
  2. Select the account.
  3. Click on View general account info
  4. Copy the ...@pages.plusgoogle.com email address there.
  5. Go to https://console.cloud.google.com/
  6. Enable the YouTube data API for my project.
  7. Download the credentials.json.
  8. Go to Data Access - Audience
  9. Set the User type to External
  10. Add my brand account as one of the Test users.
  11. Log in at the command line:

     gcloud auth application-default login \
         --client-id-file=credentials.json \
         --scopes="https://www.googleapis.com/auth/youtube"
    

Then the following code calculates the offset of the timestamp at point based on the livestream that contains it.

;;;###autoload
(defun sacha-google-youtube-stream-offset (time)
  "Return the offset from the start of the stream.
When called interactively, copy it."
  (interactive (list (sacha-org-time-at-point)))
  (when (and (stringp time)
             (string-match org-element--timestamp-regexp time))
    (setq time (org-timestamp-to-time (org-timestamp-from-string (match-string 0 time)))))
  (let ((result
         (emacstv-format-seconds (sacha-google-youtube-live-seconds-offset-from-start-of-stream
                                  time))))
    (when (called-interactively-p 'any)
      (kill-new result)
      (message "%s" result))
    result))

(defvar sacha-google-access-token nil "Cached access token.")

;;;###autoload
(defun sacha-google-access-token ()
  "Return Google access token."
  (or sacha-google-access-token
      (setq sacha-google-access-token
            (string-trim (shell-command-to-string "gcloud auth application-default print-access-token")))))

(defvar sacha-google-youtube-live-broadcasts nil "Cache.")
(defvar sacha-google-youtube-stream-offset-seconds 10 "Number of seconds to offset.")

;;;###autoload
(defun sacha-google-youtube-live-broadcasts ()
  "Return the list of broadcasts."
  (or sacha-google-youtube-live-broadcasts
      (setq sacha-google-youtube-live-broadcasts
            (request-response-data
             (request "https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&mine=true&maxResults=10"
               :headers `(("Authorization" . ,(format "Bearer %s" (sacha-google-access-token))))
               :sync t
               :parser #'json-read)))))

(defun sacha-google-youtube-live-get-broadcast-at-time (time)
  "Return the broadcast encompassing TIME."
  (seq-find
   (lambda (o)
     (or
      ;; actual
      (and
       (alist-get 'actualStartTime (alist-get 'snippet o))
       (alist-get 'actualEndTime (alist-get 'snippet o))
       (not (time-less-p time (date-to-time (alist-get 'actualStartTime (alist-get 'snippet o)))))
       (time-less-p time (date-to-time (alist-get 'actualEndTime (alist-get 'snippet o)))))
      ;; actual, not done yet
      (and
       (alist-get 'actualStartTime (alist-get 'snippet o))
       (null (alist-get 'actualEndTime (alist-get 'snippet o)))
       (not (time-less-p time (date-to-time (alist-get 'actualStartTime (alist-get 'snippet o))))))
      ;; scheduled
      (and
       (null (alist-get 'actualStartTime (alist-get 'snippet o)))
       (null (alist-get 'actualEndTime (alist-get 'snippet o)))
       (not (time-less-p time (date-to-time (alist-get 'scheduledStartTime (alist-get 'snippet o))))))))
   (sort
    (seq-filter
     (lambda (o)
       (or
        (alist-get 'actualStartTime (alist-get 'snippet o))
        (alist-get 'scheduledStartTime (alist-get 'snippet o))))
     (alist-get 'items

                (sacha-google-youtube-live-broadcasts)))
    :key (lambda (o)
           (or
            (alist-get 'actualStartTime (alist-get 'snippet o))
            (alist-get 'scheduledStartTime (alist-get 'snippet o))))
    :lessp #'string<)))

(defun sacha-google-youtube-live-seconds-offset-from-start-of-stream (wall-time)
  "Return number of seconds for WALL-TIME from the start of the stream that contains it.
Offset by `sacha-google-youtube-stream-offset-seconds'."
  (+ sacha-google-youtube-stream-offset-seconds
     (time-to-seconds
      (time-subtract
       wall-time
       (date-to-time
        (alist-get 'actualStartTime
                   (alist-get 'snippet
                              (sacha-google-youtube-live-get-broadcast-at-time wall-time))))))))

;;;###autoload
(defun sacha-google-clear-cache ()
  "Clear cached Google access tokens and data."
  (interactive)
  (setq sacha-google-access-token nil)
  (setq sacha-google-youtube-live-broadcasts nil))

For example:

(mapcar
 (lambda (o)
   (list (concat
           "vtime:"
           (sacha-google-youtube-stream-offset
            o))
         o))
 timestamps)
19:09 Getting more out of livestreams
37:09 Announcing livestreams
45:09 Processing the recordings
51:09 Non-packaged code

It's not exact, but it gets me in the right neighbourhood. Then I can use the MPV player to figure out a better timestamp if I want, and I can use my custom vtime Org link time to make those clickable when people have Javascript enabled. See YE16: Sacha and Prot talk Emacs for examples.

It could be nice to log seconds someday for even finer timestamps. Still, this is handy already!

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

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

-1:-- Make chapter markers and video time hyperlinks easier to note while I livestream (Post Sacha Chua)--L0--C0--2026-04-17T04:27:43.000Z

Sacha Chua: YE16: Sacha and Prot talk Emacs

: Updated chapter markers and transcript

In this livestream, I showed Prot what I've been doing since our last conversation about Emacs configuration and livestreaming.

  • 00:00 Opening
  • 04:24 Workflow checklist
  • 04:47 Demonstrating sacha-stream-show-message and qrencode
  • 05:54 qrencode
  • 07:55 Embark
  • 17:14 My objectives
  • 19:00 keycast-header-mode
  • 19:45 Trade-offs when livestreaming while coding
  • 21:24 Trade-offs: seeing less text on the screen
  • 23:52 Lowering the effort needed to announce a stream: Prot just announces it and the blog post embeds it
  • 24:43 Timestamps
  • 27:29 Different types of livestreams
  • 28:14 Reading other people's configs
  • 30:12 Hanging out
  • 31:40 Livestreams for explaining specific things
  • 32:00 Prot on didactic livestreams
  • 34:07 Prot suggests breadcrumbs
  • 37:59 Announcing livestreams
  • 38:58 Embeds: Prot embeds specific YouTube videos instead of the general channel one
  • 39:32 Demo of my new shortcut for converting time zones
  • 41:48 Ozzloy's questions about time zones and QR codes
  • 43:46 Prot on announcing livestreams on blogs
  • 45:25 Processing the recordings
  • 47:15 Commitment devices
  • 48:29 Automating more of the process
  • 51:14 Copying non-packaged code
  • 52:25 Prot on defcustom
  • 55:12 helpful and elisp-demos
  • 56:23 Prot on code libraries
  • 56:50 Prot rewrites functions to fit his style and naming conventions
  • 59:18 Prot's preference for small functions
  • 01:00:23 avy-goto-char-timer
  • 01:02:40 One-shot keyboard modifiers
  • 01:03:29 Toggling
  • 01:05:08 System-wide toggle shortcuts using emacsclient
  • 01:07:25 My next steps
  • 01:08:18 Tips from Prot: small functions used frequently
  • 01:09:06 Maybe using the header line for tips?
  • 01:10:23 Reorganizing keys

2026-04-16-01 Preparing for chat with Prot.jpeg

Questions I'm thinking about / areas I'm working on improving:

  • (Log) Getting more out of livestreams (for yourself or others)
    • You've mentioned that you don't really go back to your videos to listen to them. I was wondering what could make the livestreamed recordings more useful to either the person who made them, people who watched it live, or people who come across it later.
    • Tradeoffs for livestreaming:
      • Plus: debugging help, capturing your thinking out loud, conversation, sharing more practices/tips
      • Minus: Fitting less stuff on screen, distractability
    • A few types of livestreams:
    • (Log) Announcing livestreams
      • You add a post for scheduled/spontaneous livestreams and then you update it with the description; probably fine considering RSS readers - people can visit the page if it's finished
      • Debating whether to embed the channel livestream (picks next public scheduled stream, I think) or embed the specific livestream

      • Now on https://yayemacs.com (also https://sach.ac/live, https://sachachua.com/live)
      • Added timestamp translation to Embark keymap for timestamps, sacha-org-timestamp-in-time-zones
      • TODO: Post template
      • TODO: ical file
      • TODO: Easier workflow for embedding streams
      • TODO: Google API for scheduling a livestream
    • (Log) Processing the recordings
      • I like editing transcripts because that also helps me quickly split up chapters
      • Tracking chapters on the fly
      • Extracting screenshots and clips
      • Turning videos into blog posts (or vice versa)
      • TODO: Automate more of the downloading/transcription, common edits, Internet Archive uploads
  • (Log) Do you sometimes find yourself copying non-packaged code from other people? How do you like to integrate it into your config, keep references to the source, check for updates?
    • convert defvar to defcustom
    • Current approach: autoload if possible; if not, add a note to the docstring

         (use-package prot-comment                ; TODO 2026-04-16:
          :load-path "~/vendor/prot-dotfiles/emacs/.emacs.d/prot-lisp"
                :commands (prot-comment-timestamp-keyword)
                :bind
                (:map prog-mode-map
                                        ("C-x M-;" . prot-comment-timestamp-keyword)))
      
         ;;;###autoload
      (defun sacha-org-capture-region-contents-with-metadata (start end parg)
        "Write selected text between START and END to currently clocked `org-mode' entry.
      
         With PARG, kill the content instead.
         If there is no clocked task, create it as a new note in my inbox instead.
      
         From https://takeonrules.com/2022/10/16/adding-another-function-to-sacha-workflow/, modified slightly so that it creates a new entry if we are not currently clocked in."
        (interactive "r\nP")
        (let ((text (sacha-org-region-contents-get-with-metadata start end)))
          (if (car parg)
              (kill-new text)
            (org-capture-string (concat "-----\n" text)
                                (if (org-clocking-p) "c"
                                  "r")))))
      
    • prot-window: run a command in a new frame
    • Look into using keyd for tap and hold space?
    • header line format with common tips
Transcript

00:00:00 Opening

[Sacha]: This is Yay Emacs number 16. I'm Sacha Chua and today I will be talking with Prot once my alarms stop going off. Yes, yes. I'm going to be talking with Prot later, assuming that all of this stuff works. Let me double check my audio is on. Audio is definitely on. I'm trying a little bit early so that I'm not doing so much last-minute panicking. Let's see what we've got here. I am also trying the new OBS 32 interface for things, so that should be fun. Alright, thank you to phyzixlab for confirming that the audio works. I am so fairly new to this livestreaming thing, but I'm looking forward to seeing if I can do it more regularly because I have a little bit of predictable focus time between now and the end of June. In July, the kid is on summer break and so will probably want to hang out with me all the time. Or not, you know, kids are like that, right? So in the meantime, I am trying to get the hang of scheduling things and since Prot happens to have an Emacs coaching service, I figured I would engage him to coach me on live streaming and Emacs and all sorts of stuff, which is really, you know, making sure that I have somebody to talk to and bounce ideas around with and see where we end up. So the last time, which was, Yay Emacs, when was this? Yay Emacs 10, I had a coaching session with him to talk about Emacs workflows and streaming. So I've been working on modularizing my configuration. I'll explain all of this again when he comes on, but just to get the hang of this. I've modulized my config. I've gotten through hundreds of function definitions and exported them all into individual files. I have in fact even renamed them from my-whatever to sacha-whatever. So it's slightly easier to copy my functions because they won't trample over other people's custom functions called my-whatever. My background blurring is very background blurring. So that's all good. And then I've got a couple of other modifications that I've made. So I've made good progress on this very long to-do list that I had made for myself after his chat. But the kiddo is here. Oh my goodness! Okay, you're gonna go back to school and stuff? You just wanted to drop by and make a comment? Yes. Also, the teacher let me change my name, but not family. They just wanted to add a - in parenthesis. Oh, yeah. Oh, that's good. Now they can refer to you. Post my name and my nickname. Alright, I'm going to test this new thing. Interesting conflict here. The kiddo likes making cameos. I am not sure how I feel about the kiddo making cameos. Anyhow! Where are we? Okay, the mic is unmuted again.

00:04:24 Workflow checklist

[Sacha]: I am going through my checklist. I have this lovely checklist now. It includes, naturally because it's Org Mode, it includes Emacs Lisp buttons that I can just click on to get stuff running. In this case, for example, I can use obs-websocket-el to start recording and start streaming at the same time. So that's all good.

00:04:47 Demonstrating =sacha-stream-show-message= and package:qrencode

[Sacha]: And I want to double check that this message thing works. Let's go see if I can send a message to the chat. Show string. This is a test message that you can ignore. And theoretically that shows up there. That shows up in the chat with a timestamp. So people using video on demand feature where you can go back and just go playback part of the thing can go see it. It would help, of course, if I had the time. And if I expand this. You have the time in the mode line here. It's currently 10:25. But then, my Firefox... Oh, maybe I should just tell you what. I will make this above others. There you go. Fancy. Super fancy. Except this is right where the...

00:05:54 qrencode

[Sacha]: What's the QR code? The QR code just repeats the string. So this will be a little more handy if I have... Let me just double check that it does do the string properly. Come on, show me the thing. Yep. So this is my... In case you're watching this in a mobile device and I show URLs, like for example, let's bring up Prot's configuration here. Let's go to... Let's do, do, do, do, do... Prot. Yeah, here. And then if I say show string and I give it the URL, then it gives you the string and the URL should be in the QR code. So people who are watching mobile. You can do that. People who are in the chat can get it from the chat. It's timestamped so that if I grab the timestamps later on, I can use that sort of for chapters. And just generally all these little conveniences. This QR code is provided by the qrencode package. So it's in Emacs. It's actually characters. There's probably a way to just insert the image. But I thought it was cool. I can't remember who had this technique in one of his videos. Maybe it was John Kitchin? That seems like the sort of thing he might do. Or it might be someone else. Anyway, just these little conveniences because copying text, especially in mobile, or trying to type things... Try to pause the video at just the right moment. It's very annoying. Eventually, I would like to plug it into all the usual Embark stuff. For example, you'll see this later as I go through this stuff with Prot. Log buttons will show messages.

00:07:55 Embark

[Sacha]: But theoretically, it would be nice to have my Embark here. For example, I'm on Embark on an org URL link. It makes sense that... Wait a minute, I do have it. Okay, I think I have it on Z here. Is that a capital Z or a small z? Let's find out. Z? Not a small z. Capital Z. Whoa, look at that! Okay, okay, so I already do have it. Embark is a package that lets you have context-sensitive keyboard shortcuts. And so I have this now mapped so that if I want an org link, I can press control dot and Z and it will send it to the chat and display it on the screen with a message because who wants to type things manually? You know, this is Emacs. We don't do anything manually. And then theoretically, that also should show up in... Look at that! It's showing up over here in my timestamp section using the magic of org-capture. It includes a timestamp and then, of course, with a little bit of math, I can calculate this as an offset into the streaming video file because I started the stream probably at the same time. Anyway, just a little bit of math to calculate that. And then I can get chapters out of it. Theoretically. Or I could use that to index into the transcript and edit things. Hello, Prot! Hello! We are already live. I have just been on screen.

[Prot]: Already live! Great. Yes.

[Sacha]: Panicking. Not panicking. Experimenting with all the fun stuff. I'm now going to share my screen with you so that you can see also. Select window. Let's go to all of it. Screen one? Screen one. I think it's screen one. Okay. Allow. So, theoretically, you should see my screen.

[Prot]: Very well, very well. Looks good, looks good. We have connectivity issues, it seems.

[Sacha]: Your audio sounds choppy.

[Prot]: Yeah, same here. I cannot hear you well. Can you hear me now?

[Sacha]: I dropped my performance.

[Prot]: Okay, okay, do that. Well, very well. Because it seems that our... Yes, okay, I did the same. Okay, so hopefully this will work. Let's see.

[Sacha]: It's an experiment.

[Prot]: It seems more stable now.

[Sacha]: Yes, this is one of the reasons why we're having these sessions, so that you can experiment to see what's possible. And I was just telling stream that I've been having a lot of fun tinkering with a lot of the ideas that I was working on after the last chat two weeks ago. So my goal for this session is to not panic.

[Prot]: I really cannot hear you clearly. I keep getting interruptions, so... It seems that... Yeah, I don't know what we could do. Maybe I can try to leave and rejoin, maybe. Let me exit and rejoin Jitsi, maybe that will fix it. Okay,

[Sacha]: let's try that. Okay, so let me do that very quickly. Quite possibly, I am asking my computer to do too many things. Let's see. I am asking my computer to do too many things, audio-wise.

[Prot]: Okay, we will see. We will find out.

[Sacha]: Let me try changing my virtual mic. How about this one?

[Prot]: No, your audio is still kind of choppy. Why is your audio choppy?

[Sacha]: Let's see. What do you think? Yeti, monitor your audio. Let me check. Not good. It's okay. Live debugging. Here we go. Okay, you are, where are we? You are Firefox. Yes, yes, yes. Okay, I can disconnect the, uh, disconnect the connections. Let me think. Connect the ports of Combined Sink Monitor to Firefox Input.

[Prot]: And while you do that, we will... Testing.

[Sacha]: How are we doing?

[Prot]: There it is.

[Sacha]: Is this slightly better? Testing. One, two, three.

[Prot]: Yeah, let's see here, so... Okay,

[Sacha]: that seems to be good. And now I'm sharing my screen. How is our screen? Hmm, does not like screen sharing at the same time. Let me see what's going on with my memory. My memory is fine. I have memory. Let us stop the screen sharing. How are we now? Is our audio back?

[Prot]: Okay. I can hear you well. I can hear you well in terms of the fact that there is no choppiness now in the audio. However, your voice has been distorted a little bit. It's not a problem. I can hear you clearly, but I just mention it for the sake of your setup.

[Sacha]: This is interesting and I'm not entirely sure how I will go about fixing it at this moment. No problem. It's not really a problem because I hear you well,

[Prot]: so that's enough. I am tempted to suggest the non-free...

[Sacha]: Let's jump over to Google Meet and see if that's any better.

[Prot]: Let's do it. Send me the link and let's do that. No problem. We are already on YouTube anyways. Let me try this. [Sacha] I will send it to you in the Jitsi chat and then things will be crazy.

[Sacha]: It's in the Jitsi chat and we'll see if that works. Does that work? I will also email it to you. That's not the link. Okay. Now I need to see whether this actually works. Oh. Ah! Ah, technology! How does it work? Camera is starting. Camera is not starting. I don't know what it's talking about. Camera is starting. Allow camera. Join now. Okay. Testing. My audio works. Admit one guest. Admit. Okay. Testing. Does this work now? I can hear you clearly. Okay. Now I'm going to try sharing this. Yes. Very

[Prot]: well. And then let's see what happens. Share. Yeah. The moment of truth. Let's see.

[Sacha]: Technology continues to work?

[Prot]: Yeah, yeah, it does work. This is smooth. This works. So let's see. Okay, all right. So it probably means that in the

[Sacha]: future I might actually need to spin up our Big Blue Button server because sometimes the free Jitsi, you know, you're just dealing with whatever you get for free, right? We already have comments. phyzixlab wants to know, well, phyzixlab says, Prot, I'm jealous of your beard. Which Emacs package can I install to have a glorious beard like you? Emacs Genes. Emacs Genes. Y'all can book your own coaching session with Prat. Although technically, I don't mind sharing mine.

00:17:14 My objectives

[Sacha]: Okay, so my objectives is I want to capture and share more, right? And that's great because in the experiments that I've been doing with live streaming so far, I have found myself going on tangents based on people's questions. And theoretically, I can go back and use those transcripts, which I haven't yet. But that could be more stuff into blog posts that are more searchable. And creating opportunities for conversation, which I think you've also been experiencing with your experiments with live streams lately. Because it is nice to have that back and forth when you're demonstrating something and you can immediately show something that was unclear. Quick overview of my timeline. Again, until June, I've got a fairly predictable schedule, except for the times when the kid turns out to have a substitute teacher and is too grumpy to go to school. So just some flexibility still with the schedule, but I am starting to experiment with scheduling chats. So that's nice. And this is our first experiment with it. I'm like, okay, let's try a live stream at this date at this time with somebody who is going to show up also. And then in July and August, since my schedule will be less predictable, then we'll do more spontaneous things like we also have been doing. And then September onwards is probably going to be EmacsConf. So with that in mind, I want to quickly share the updates from the last one. And probably, you know, you will think about stuff and say, oh, yeah, have you thought about doing this? Or, oh, that's good. Try this one next. Or in my experience, so and so and so. And of course, I'd love to hear what you've been learning about also.

[Prot]: Yeah, yeah, yeah, yeah, yeah. Very good.

00:18:59 keycast-header-mode

[Prot]: And I will tell you my experience as well, because based on our last exchange, I also tried keycast at the top, for example.

[Sacha]: Yeah, yeah. It gets out of the way of the closed captions.

[Prot]: It does. It does. Yeah. So it has some advantages and it's always visible and the key and the command is always visible. But I have to get used to it because it was distracting me.

[Sacha]: Yeah, I hear you, I hear you. It's kind of a trade-off, right? And that actually goes to one of the points that I wanted to touch on later where getting the hang of live streaming while coding or while working does require a fair bit of trade-offs. On the plus side, I'm going to see if this works. It should insert a chapter marker so

00:19:45 Trade-offs when livestreaming while coding

[Sacha]: that I know, okay, this part to this part is this conversation. So when you're live streaming while you're doing package maintenance or you're working on config or whatever else, it is slightly more distracting because people come up with interesting comments and conversations. But on the plus side, it is also, as I've seen you do, helpful at debugging. You're staring at something. You're like, what's wrong here? And someone is like, oh yeah, you're missing a trailing slash.

[Prot]: Yes, yes. It really helps. Well, I'm not sure if it helps, though, because the fact that you are talking to the chat means that you are not paying attention to what is in front of you. So it can cut both ways, right? There are times, though, where it really helps. Yes. Where you are completely lost and then the people in the chat are like, hey, that's how you fix it.

[Sacha]: All right. So maybe I just have to A, build up more of a conversation so that we can get those benefits and B, figure out how to run my narration on a separate worker thread in my brain. I don't think it happens. I think I used to be more multithreaded in the past, but I am slightly less multithreaded now. However, it turns out that spending all this time with kids means I am getting better at generating verbal responses that I'm not necessarily, you know, like focusing too much on or just saying like stuff to keep them amused and entertained. Oh, that's quite a skill. Yes,

[Prot]: that's good. That's good. I don't know. But yeah, so there

[Sacha]: are trade-offs here.

00:21:24 Trade-offs: seeing less text on the screen

[Sacha]: The other thing is now that I am using mode to switch on my... I am streaming, do the Fontaine preset and all of that stuff. Now there's like less space on my screen for code. So I had to get used to it again. yes yes yes

[Prot]: yes that that's one of the downsides of course yes like you have to have a larger font so that people can see what you are typing and then of course that comes at the cost of including fewer things on screen Though maybe you could have a little bit of a wider frame, like specifically in your case. I don't know, it's already at the 80 characters already? Yeah, it's already... Yeah, I think in my case, my frame fits about 100 characters. Well, I haven't measured it, but I think it's something in that... Like, yeah, about there is my frame.

[Sacha]: Yeah, it has about 80 characters. So it's about 75 characters.

[Prot]: So in my case...

[Sacha]: All right. And then the stream can tell me if this is still readable, because of course more code on the screen means more code getting written or done.

[Prot]: And just to say also more code on the screen means that it can be easier to debug or write the code. Because you have the context right there. You don't have to go up and down the screen to find it.

[Sacha]: Especially since I'm used to actually dividing my frame into two windows so I can do left and right. And I'm doing this on a standard aspect mode. You have a widescreen, so you're a little bit spoiled in this regard. I only have like two monitors that I'm doing. But maybe that is what I'll end up just using separate frames for. Yes, so slightly smaller font size, and stream can tell me whether this is too small for them. I know people who are older will develop an appreciation for larger font also, so take advantage of this ability to work with medium-sized fonts while they can. So font sets, that's definitely a thing. And then just trying to figure out how I can make it more useful both to other people and for myself and during the live stream as well as after the live stream.

00:23:52 Lowering the effort needed to announce a stream: Prot just announces it and the blog post embeds it

[Sacha]: Now you've mentioned you don't actually go back into your live streams afterwards. You just plug the YouTube video, you update your description so that it's past tense instead of future tense and you republish your post. I think that's your workflow, right? Even less. So I don't even retrofit the

[Prot]: past tense, you know, present tense to past tense. It's like all present tense. It's like I will do a live stream. It will be recorded. You can find it here kind of thing. Okay.

[Sacha]: All right.

[Prot]: And so just to say, though, just to say the reason I do this is because I don't want to go through a three hour stream again because then a three hour stream becomes like a ten hour stream in practice. And this means that it adds friction and it adds to the requirements, which effectively means I will be doing fewer of them. Yeah.

00:24:43 Timestamps

[Sacha]: That's what I'm thinking. Maybe lightweight sort of chapter markers. You've mentioned you just remember this sort of stuff, but since I don't actually remember this sort of stuff, having a way for Emacs to send messages to the stream and also show things in the timestamps. I have a timestamp now. It's nice. It just says Org Capture. And all that will then theoretically make it easier for me to say, okay, let's go find the chapter and then I'll just adjust the timestamps afterwards to say, okay, from this point to this point. If people are interested, they can go in there and they can look at the transcript for more.

[Prot]: I think we discussed this last time as well. You could have a function like start-stream and it starts a timer or it starts recording the time and then relative to that point, any offset and that's your timestamp right away. And whenever there is some event happening, you can type a key and then maybe it gives you a prompt and you write what is it, like just a string and then that is the chapter.

[Sacha]: An org timer will do that kind of insert a timestamp for you. But one of the reasons why I liked having my custom show message thing is that it can display the text on the screen, display a QR code for the text in case people want to copy the function that I'm talking about, send it to the chat so that people using video on demand can say, oh yeah, at around 10:25 or whatever. I'm currently using wall-clock timestamps, which means I need to modify my mode line so that the time starts earlier and people can use that to jump around the thing. And then, so it's like in half a dozen places, which is what org-timer does not get me if I'm just inserting a timestamp here. Anyway, minor, like, you know, little workflow improvements. But it's this whole, as you said, I don't want to go back and spend six hours processing the three-hour livestream. I want to say, all right, this video has some potential interesting things here because these people ask these questions. This is roughly the time when I answer those questions. Ideally, this is the text of the question. Someday, there might even be screenshots and clips. I'm modifying compile-media to make it easier for me to do that kind of video editing from within Emacs.

[Prot]: Oh, wonderful.

[Sacha]: yeah, yeah. But it's all still like, okay, progress. First, I've got to develop the habit of streaming, and then I have to develop the habit of saying, now we are talking about this topic so that it can all get marked everywhere.

00:27:29 Different types of livestreams

[Sacha]: And that got me to thinking, well, there are a couple of different types of live streams and you might have also done something about which ones fit the way that you had to present. One is the, you know, the, I'm going to spend time doing this anyway, which is like your package maintenance, where you will accept a little bit of distractibility for the benefit of having other people around to ask questions and clarify things and stop you when you're getting stuck somewhere. I have something I specifically want to teach and you've done this before with walking through a blog post and just demonstrating things interactively because there's some things that are easier when you're showing it, right?

[Prot]: Correct, correct. ...

00:28:14 Reading other people's configs

[Sacha]: Reacting to other things. In this one, I've started to have fun with because I've been going through your Emacs configuration, which is several hundred pages when converted to a PDF. And I forget, do you actually, like, do you produce a PDF, PDF, like a nicely thingy?

[Prot]: I haven't done it, but that's trivial to do, actually. I could do it.

[Sacha]: Yeah, yeah, so I've also been reading tecosaur's PDF, and his PDF is gorgeous. Like, it starts off with, like, a cover page and and everything. But it's Doom Emacs. I have to translate a lot of things to my specific setup. But now I have literate config envy. Anyway, that's an entire category of live streams here, which could just be me copying interesting things out of other people's configs. Today we are experimenting with a chatting with a guest variety of live stream, which you also do with your Prot asks. Actually, I forget. Are those live streams?

[Prot]: They are not live streamed, but the idea is that I do not edit them. However, if somebody really wants, I can edit it. So the idea is let's go with the flow. Don't worry about it. It's casual, all that. But if somebody says something that doesn't sound right, doesn't mean it or whatever, I'm happy to edit it.

[Sacha]: Yeah. I'm starting to look into how to do that if I'm doing this live and apparently if I set up a sufficiently long buffer in OBS for streaming, like a delay for 20 seconds or 15 seconds, then I can stop streaming and the stuff that happened in the last 10 or 15 seconds doesn't make it out to the public, but it's still kind of...

[Prot]: Living dangerously, yeah.

[Sacha]: Yeah, yeah. Because seeing as I'm still practicing remembering to flip the webcam down when the kid runs in and wants to be on camera, I'm like... My reaction time, not there yet.

00:30:12 Hanging out

[Sacha]: And then other people are like, they just hang out. They're not like, I'm going to do something. They're just hanging out, which I'm sort of starting to experiment with when I'm doing Emacs News on Mondays, because I'm like, I'm categorizing it anyway, but it doesn't require a lot of brainpower because I'm not coding or debugging. I'm just saying, okay, this looks like an Org Mode link. This looks like a miscellaneous link. And then some people just play games, which is fun too.

[Prot]: Yes, that's good. And they want to have somebody on the side, guide them through what they are doing.

[Sacha]: Yeah, or it blends into a hanging out sort of thing. Yes, yes. And it's like, what is the kiddo doing now?

[Prot]: Yeah, the camera, the camera. That's fun, that's fun. Good reaction time. Yeah, yeah, yeah.

[Sacha]: Yes, thank you for your homework. I will scan this and put it online later. This is it. Yes, life. Life.

[Prot]: Putting your reaction time to the test.

[Sacha]: Yes. So in terms of getting more out of livestreams, That's what I've been thinking about lately. I think I would like to do more of these, you know, hey, folks, keep keeping company while I'm coding this or whatever, since you've been having a lot of good experience with that.

00:31:40 Livestreams for explaining specific things

[Sacha]: I would also like to eventually move into more of these. I have something I specifically want to demonstrate, which probably necessitates actually organizing my thoughts. And you've done a bunch of these. After writing a post, it seems like more like recording a video and walking through it. Do you also sometimes do them before writing a post?

00:32:00 Prot on didactic livestreams

[Prot]: I haven't done that but actually, when I write posts, I write them in one go, so maybe I should do a live stream where I actually write a blog post just to show that I can do it. The thing is of course what do you want to communicate, because if it's teaching, like if you are writing it and trying to teach it at the same time, there is a chance that you might leave something out. Some of that detail, some of that nuance. For example, if you want to explain how a form in Emacs Lisp works, let's say if or cond, you may not come up with a very good example live and it may not have didactic value. So even though you know how it works, the communication value is not there. So that it helps for you to write it in advance. Even if it's in one go, again, you can write it, you can read it, and then you can come up with a good example and then stream that. So it really depends on what you want to do. The other day I did a stream, a live stream, where I was writing a package from scratch, a small package. So there part of it was to teach, but also to demonstrate. And there I don't really care if the didactic value is very high. Because even if there are mistakes, it's part of the process. It's not like, well, you will come here and from zero to hero kind of thing, you will learn everything. It's not like that. It's like you come here, you might learn something, but the bar is relatively low.

[Sacha]: I think especially since my mind likes to jump around a lot-- you seem a lot more organized when you're thinking through things, especially if you're saying you write your blog posts straight in one go. I'm like, okay, do this part over here, do that part there. I will definitely lose things, like you mentioned, and I will definitely go back and say, no, I need to do this before I can say that. So yeah, I think I can save that for summer when I might be focusing more on things I cannot schedule.

00:34:07 Prot suggests breadcrumbs

[Prot]: How about leaving breadcrumbs for yourself? Like, I was writing this. Like, write a comment. Basically, I was writing this, I need to remember that, and then you jump off on the tangent.

[Sacha]: I need to use a universal prefix to get the time, don't I? Yes. Leaving yourself breadcrumbs. Yeah, yeah, yeah.

[Prot]: And then you can retrace your thoughts, basically. Like, okay, I was here, I was meaning to do that. Especially when you are streaming, chances are that there will be several comments that are very interesting and you want to get to. And you might be talking to them for 10 minutes or more. And then, of course, if you don't have that or you want to jump off on a tangent, you will eventually forget what you were doing.

[Sacha]: Do you have anything like this already that you're currently doing?

[Prot]: And no, but this is the sort of thing that should be a fun exercise to actually demonstrate as well for yourself.

[Sacha]: I use ZZZ if I just put it in text and I have some things, for example, in my message hooks so I can't send email that contains this. And of course, org has its whole clocking and interrupting tasks that I can use. I just have to have the presence of mind to actually say, oh yeah, now I'm going to go on this tangent and I want to go back to this later on. Leaving myself breadcrumbs is definitely something I need to formalize into workflows that I actually use.

[Prot]: Yeah, that's the thing. And you can also benefit. I don't know. Of course, that's depending on if you are a visual person or not. But you could also rely on color or, for example, include an emoji as well or modify font-lock-keywords to have like something that stands out. Basically, make it clear that, well, this is an interjection. I will just go and then I will be back. Yeah.

[Sacha]: Good idea. Okay. So that will definitely help with the things where maybe I want to demonstrate something and I want to do the thinking out loud so that it's recorded. And just in case other people have any questions, they can come by and ask them. And then I can sort of massage it into a proper blog post, but still leave the link to the video in case people want to hear the stream of consciousness figuring out of all of this stuff. That sounds like maybe a more polished video or blog post with screenshots and clips coming out of this livestream ramble, kind of tangled. Okay, we're going to jump over here. Gotta leave myself a breadcrumb because I'm going to go in this detour to answer someone's question.

[Prot]: There is value to both. There is value to both because the live stream is a stream of consciousness. You can think of it like a bubbling effect. There is fermentation going on, a lot of things happening. And then when you publish the polished, the finished article, that's the distillation effect. So fermentation distillation. So both are useful. Both is good to see and have a sense of what they are up to, what they are doing. Yeah.

[Sacha]: And Charlie in the comments says he likes Emacs' excursions terminology. So if you can think of it as a save excursion, I'm going to go do something and then come back. I am not very good at popping the stack, but I will work on it. Yes. A couple of other things that I want you to pick your brain about. So you mentioned that in terms of announcing live streams, you're like, look, I'm remembering to mark a topic change.

00:37:59 Announcing livestreams

[Sacha]: So you mentioned, okay, you have a post for the scheduled or spontaneous live streams. Then you actually, you don't even update it with the description. You write the description beforehand and you leave it alone. Probably when people get it in their RSS reader, I guess the YouTube embed always just points to, you know, it's either the currently playing live stream or the archived recording of it. And that's that. The link is the same. The link is the

[Prot]: same. Yes. Yeah, on this live page. So now I have

[Sacha]: yayemacs.com and SachaChua.com/live pointing to this page. And there's like, there's a YouTube way to embed just like upcoming live stream, but then it's like fiddly when it comes to, oh, you know, you've got, if you have more than one public up scheduled live stream or whatever, do you use any of this stuff at all where you're like saying a page that's always has your upcoming or current stuff?

00:38:58 Embeds: Prot embeds specific YouTube videos instead of the general channel one

[Prot]: No, I have a generic embed which I copied many, many years ago and I have it in my static site generator. Then the only field that changes is the ID of the video. And this works for live streams as well as pre-recorded videos.

[Sacha]: Okay, so you always give it like the video IDs basically.

[Prot]: The video ID, yes. I can share with you the exact snippet.

[Sacha]: Yeah, yeah. That would be, you know, and you can send...

[Prot]: Yeah. Well, it's public anyway.

[Sacha]: I can steal it off your website. It's fine.

00:39:32 Demo of my new shortcut for converting time zones

[Sacha]: And then I have just added timestamp translation as well. So I can say, okay, you know, let me show it to you. So this is my webpage, right? So here, this is your standard org timestamp. Yeah. And if I open up https://sachachua.com/live, it's also the same as Emacs. Okay, okay, okay. And I find the browser window. Okay. Theoretically, if I say, okay, down here, you click on this, it translates it to your language. Ah, nice,

[Prot]: Nice, nice.

[Sacha]: Because YouTube will do that for the upcoming one if people link to it. But, you know, it's just people. But this is JavaScript, anyhow. And the other thing that I have just added today is I can go onto that in Org. If I press my control dot embark thing, I can use my Sacha Org timestamp in time zones, which is shift W. And it translates it into a gazillion time zones. So then I can mastodon toot it, which I did,

[Prot]: Just to say that copy to the kill ring, okay, yes, okay, good, good, good.

[Sacha]: Because time zones suck. I mean, it's great, but I cannot do the translation and so I am slightly... I'm working on announcing those upcoming scheduled streams while doing all the math so that... well, having emacs do all the math so that I don't have to do the math.

[Prot]: Yes, that's the spirit. That's good. Very good. This is very nice. Is this timestamp always meant for Mastodon or do you have it elsewhere? I think I've seen it in the Emacs news as well.

[Sacha]: Oh yeah, I'm basically stealing the code. I've used it in Emacs Conf and for Emacs News. I used to announce the Emacs News events also. I should get back to doing that. But definitely in the Emacs News and Emacs Calendar, I translate all of the events into multiple time zones for the virtual ones.

00:41:48 Ozzloy's questions about time zones and QR codes

[Sacha]: Line 23 doesn't have a time offset. Okay, someone is commenting. Ozzloy will tell me about it a little bit later. Ozzloy also has a question. Am I creating the QR code with Emacs Lisp? Is it actually text in Emacs? I'm going to go on a quick detour to show the QR code. Yes, do it, do it, do it. By

[Prot]: the way, I will like the stream. I didn't have the chance to do that. A show string. Yes. So here, this is my... Look, I'm

[Sacha]: using line numbers, but they're really long. Yeah, these

[Prot]: are massive. Of course. What can we do? But it's still better because I can say, okay, go to 97, right? And you kind of know where I mean. Yeah. Yeah, so this is qrencode, qrencode

[Sacha]: format, and all of that stuff. It is in Emacs. I think this one actually inserts text. There's probably a way to get it to get images as well. But yeah, so QR codes, because why not?

[Prot]: Yeah, no, that's very efficient. Yeah, yeah, good, good.

[Sacha]: Okay. Yes. So these timestamps are basically in my local time, and then I can translate them to other time zones, and then I can start announcing them, which will probably happen more if I can get my GotoSocial Mastodon thing to be more reliable. But also following your example, I should try putting it in my blog. I just feel like a little weird suddenly going from posting on my blog like once or twice, well, two or three times a week to Hey, OK, every day. All right. In ten minutes, you're going to have a live stream of me talking about random stuff.

00:43:46 Prot on announcing livestreams on blogs

[Prot]: Well, in a sense, it is weird because it's not something you would normally do on a blog, right? Like you have been blogging for a long time and you know how blogging is, right? You just do it on your own. But this streaming culture is a different experience. I think, however, it shares a lot with the blogging way of doing things, which is like, well, this is what I have to say. This is what I think. And I just do it in a slightly different format. And of course, because you are doing the stream, ultimately you control how you participate, to the degree that you participate, what you want to comment on. So ultimately, even though it's a live stream, you can control it in a way that is not that much of a live stream. In the sense that you can be very specific, very structured and be like, you know what, this is my structure, this is what I will do, and I will not run off on a tangent, for example.

[Sacha]: I don't know if it is possible for me to not run off on a tangent. I appreciate people who can be very focused. It's okay. I think my job, I think my goal is more of how do I at least describe the tangents in text form so that I can find them again and so that other people can decide whether this is worth two hours of their time or whether they can just skip to the five minutes that concerns the thing that they like.

[Prot]: Yes, in that case the timestamping would be the way to go. Timestamp plus a brief description.

[Sacha]: Yes, yes, and that actually gets me to... ta-da!

00:45:25 Processing the recordings

[Sacha]: topic: processing the recordings So, yes, as I mentioned, I've been enjoying going back and editing the transcripts because it becomes an excuse to tinker with Emacs and subed-mode, and then because I have this thing for adding a note above the start of a chapter, I can then easily use that to extract the chapter markers for YouTube and all of that stuff. As I mentioned, I'm working on some workflows for tracking chapters on the fly. You know, it's actually really nice having this little button. I used to think, okay, I can just press a keyboard shortcut, but apparently I forget all of my keyboard shortcuts when I'm trying to talk at the same time. So if there's a button, I'm like, I get incentivized to click on it to see whether my code still works.

[Prot]: Plus it functions as a reminder.

[Sacha]: Yes. So it's very helpful that way. And then, as I mentioned, I still need to work on a good workflow for extracting the screenshots and clips so that I can then turn it into blog posts later on and so forth. Right now, I have a pretty manual process for, okay, after the video is posted, I'm going to download it. I have some shell scripts now and the next step of course after this one is going to write an Emacs function that actually and I just finished this part. I have an Emacs function that calls the shell scripts to download the thing using yt-dlp and then start the transcription process but I still manually do the upload to internet archive which I know has a CLI tool so that's next in my list, and fix subtitles and all that stuff, so that's kind of... if I want to get more out of the recordings, that's a general direction I'm going.

00:47:15 Commitment devices

[Sacha]: This is not something that you're currently fiddling with.

[Prot]: Basically, I'm the wrong person for this.

[Sacha]: Yeah, it's okay. And part of these conversations is not so much that I'm looking to you for specific advice on things that you explicitly don't do because it would be against the alla prima. Just get it done and lower the barrier going in. But it's also useful as a commitment device for me to say, alright, I would like to get better at this. I am telling Prot in order to be able to demonstrate the stuff and make myself... If I'm going to see him in another two weeks... Am I going to see you in another two weeks?

[Prot]: Yes, yes, yes. And I will ask. I keep receipts. Yes, yes, yes.

[Sacha]: Exactly, right? So this is also valuable for that. Not just hoping that in your config, which I have now read, that you would have a snippet exactly for this purpose, but more like, okay, I'm telling somebody I'm going to do it, which means I got to go do it.

[Prot]: Yes, yes. And of course, just verbalizing it means that you can also understand it a little bit better. And you start thinking about it. And then it's a matter of writing the code.

00:48:29 Automating more of the process

[Prot]: I'm curious, though, why do you have the shell scripts and not bring all of that into Emacs? What's the advantage of having Emacs called the shell scripts? Or was it just more convenient?

[Sacha]: It's just out of convenience. Emacs does call the shell scripts. The shell scripts are there just in case I happen to be SSH-ing in from my phone. Because I'm downstairs or whatever and then I can just run it from the shell also because I use it not just for my... So I have some shell scripts for downloading the video as an MP3 or as an MP4 or as the subtitles. And so these are generally useful things that I might not necessarily remember to be in Emacs for. So that's definitely, you know... I needed to find this whole process that eventually ends up in a blog post that has all my lovely stuff. where this chat that I have with you is kind of my high-water mark of this is really fun. I would like to do more things like this, where it ends up with transcripts, resources, kind of like the show notes chapter marker indexes. These are automatically extracted from the transcript. Rough notes that we were working on there. The session ... The transcript has speaker diarization. In a video, I got your subtitles to show up in italics and my subtitles to show up in plain text. So now that I have this infrastructure, I feel compelled to make sure I schedule conversations with people so that I use it.

[Prot]: Yes, of course. And that's actually a good reason generally for writing code, ultimately, because it's the vehicle for doing what the code is supposed to facilitate. So the code is just a pretext for actually doing the thing.

[Sacha]: Or the other way around, yeah.

[Prot]: Or it can be the other way around. So the code is the goal, yeah.

[Sacha]: Yeah, yeah, I know. EmacsConf is basically the way that I test emacsconf.el. Hi. It's fine. It's fine. Yeah, so that's my thing for processing recordings. Changing topic. The button. The button. The button. We must press the button.

00:51:14 Copying non-packaged code

[Sacha]: Non-packaged code. So now that I've modularized my Emacs configuration, I've split all the defuns into different files. I have renamed everything from my- to sacha- so that I don't step on other people's function definitions. Now I'm starting to copy things from other people's code to see whether this is actually a viable approach. So this is the way I'm currently stealing something from your prot-comment. Is this sort of like... It seems to work when I go into something. If I go into something, I can press C-x M-; and it does the thing that you define. So this is sort of what you had in mind, right?

[Prot]: This is basically what I was thinking earlier with the comment. Yeah.

[Sacha]: And then theoretically, this sort of structure will also work for other people who have checked out my very large config and they can autoload specific commands out of it and then they can bind key bindings without necessarily importing all of my other set queues and add hooks because that's in a separate file now. The only thing in my list is defuns.

00:52:25 Prot on defcustom

[Prot]: And if you also, just to add, if you also have configurations for your packages, right? You can also have defcustoms for there, maybe with a default value that works for you or with a default value that is generally useful. And then you can also separate that out. So users don't have to pull anything from your configuration, but just pull the package.

[Sacha]: So right now I have... Right now I have my configurations as defvars because I'm lazy. Do you happen to have a function or whatever that you like to use to just convert a defvar into a defcustom?

[Prot]: I haven't done it because it's actually tricky with the type.

[Sacha]: Yes.

[Prot]: You know, the defcustom has the type keyword. And of course, for the most trivial cases, this is easy. Like, OK, it's boolean or it's a string or whatever. But usually it's not that simple. Like if you have an alist, you have to describe what are the key and value pairs or whatever and the elements of the alist. So I haven't done that because it's always on a case by case basis. And many of the defcustom I have will have like a bespoke type because the data structure is really specific. You know, the value they expect. For example, if you are doing something with the action alists of display buffer, like they have a really specific type how you write it.

[Sacha]: Yeah, yeah, I hear you. So I think because I have a lot of strings, I probably can get away with something that just reads the form, smooshes it into a string, adds a string, or possibly what this will end up looking like is maybe a completing read on the type of the function. Sorry, the type of the thing. And then I can just select from several types.

[Prot]: Well, you can make it like you can make it a guess. Like, of course, if this thing is quoted and it's a symbol, it's not a list. Maybe I can have like a choice or a repeat symbol or something like you. You can, but it won't be accurate. Like that would be like for you to fill it in later.

[Sacha]: Yeah. No, I was thinking just more along the lines of Like a completion so that you can select from maybe some of your common types. The actual guessing of what type it is would be an exercise left for future me. But even just not having to remember exactly what the syntax is for repeat would be nice.

[Prot]: Actually, that's good.

00:55:12 helpful and elisp-demos

[Sacha]: Yes. I mean, one of the things that I always find helpful is, like, I think I've got some examples now. I'm using helpful, right? And I'm also using this elisp-demos. So it just tells me, like, I can add more notes here and I can say, okay, this is what a defcustom, that's a repeat of a string or what a const looks like, so that... 'Cause the manual doesn't have a lot of examples sometimes. Sometimes it's annoying to dig through it looking for examples. Usually it has no examples. I think that that's...

[Prot]: if there was one area of improvement, it's that. Keep it as is, because it's high quality, but complement it with examples.

[Sacha]: I mean, technically, all of Emacs is an example, and you can just find something, but...

[Prot]: Yeah, that's why you have the manual, because if I have to dig through thousands of lines of Emacs Lisp, that will take a toll on my patience.

[Sacha]: Yeah, so for anyone who's watching, helpful and elisp-demos is how to add these helpful little notes to your describe-function, because who remembers these things?

[Prot]: Yeah, yeah, yeah. That's very good. That's very good. Yes.

00:56:23 Prot on code libraries

[Prot]: Just to say on the point, if you have packages, this is something I actually do. I just go and reference one of my packages, which I know I have done the research for. So I'm like, okay, how do you do the display buffer action alist type? I will just go to, for example, denote and copy it.

[Sacha]: I will eventually build up a list of examples that I can refer to.

00:56:50 Prot rewrites functions to fit his style and naming conventions

[Sacha]: The other question I had though was do you ever find yourself copying code from people who do not have their You know, they're functions in nice little things that you can just import and autoload. And what do you do about it? Like if they're, you know, let's say they named it, then maybe they named it without the prefix. So it might be possible to confuse it with the standard stuff or they, you know, it's mixed in with the rest of their config so you can just load the file. What do you like doing when you are copying that kind of code?

[Prot]: I will basically check if I can make edits to it. The first thing I would make is probably change the style to be like my style. So I would anyway change it so there is no scenario where I would just copy it verbatim and paste it.

[Sacha]: Okay, so you like to rewrite things and then you fit it into your naming convention because it is now yours.

[Prot]: But also like the style. For example, this function you have over there, like Sacha here, like the one we are seeing now on screen. For example, I would change the name of pargs. Not because it's wrong, but because stylistically it's not what I would write. Then I would change the indentation. Org Capture String, I would put the concat, the line below. I would basically do small tweaks, not because it's wrong what you have, but because stylistically I have a different way of expressing it.

[Sacha]: Yeah, yeah, yeah. Absolutely. I've started to add where I got it from in the docstring instead of... I used to put it in the comment. But as you mentioned, the doc strings are a little bit more visible. So then I usually don't end up looking for updates. But at least theoretically, if I do want to, I could find out who was... Or if I want to credit somebody or see what else they've come up with lately, then at least it's there.

[Prot]: Yes, it's good enough. Plus, when we are talking about these smaller functions, having the link there, I think, is enough. Like, you wouldn't need to go search for updates or whatever. Like, if they have made some changes, chances are it's there.

[Sacha]: Yeah. Okay, so rewrite things, make it fit your style, and add stuff to the docstring because you like to have thorough docstrings.

00:59:18 Prot's preference for small functions

[Prot]: Yeah, yeah, yeah. There are many functions I have where the docstring is longer than the code. I would say, yeah, many of them are like that. But also, just to say, it's because of how I will write the code, where there are many small functions building up to a big one. And so then the docstring explains basically what all these small functions contribute to.

[Sacha]: I like small functions too because I got used to coding on even smaller screens, right? And so anything that could just actually fit in the screen was much better than things that I had to page through. And it gives you many more avenues to modify the behavior because you have more places that you could def-advice, sorry, advice-add :around or whatever.

[Prot]: Actually, this is why I started doing it as well, because it's easier. I had this reason myself. I think it was an org function, which is like 200 lines, and I wanted to really change one thing and I had to copy the whole function. And I'm like, well, if this was a helper function, I would be done by just overriding the helper and I would be good.

01:00:23 avy-goto-char-timer

[Sacha]: I am slowly getting the hang of using avy-goto-char-timer so that I can copy the symbols from elsewhere. Because even if I'm using nameless to insert the prefixes and then I'm using dabbrev-expand or hippie-expand, for which the config I still need to fiddle with to make it absolutely perfect. It's still a lot of typing sometimes, since we like to use long function names.

[Prot]: And which timer variant do you use? Because it has, with two characters, it has the 0 one, which is type as much as you can within a certain time window.

[Sacha]: That's a good question. Where is this?

[Prot]: Char timer. I think this is based on... I think this is the zero. Yeah, I'm not sure. I remember it's called zero.

[Sacha]: So like I can type li and then go to like lj to jump to that one and now I have it so that I can M-j li and then I can press the yank yeah like y like insert from there which is yes when I was when I was stealing stuff from your config, I could... oh let me show you... where is this... So this is your config, right? Well, this is... Hang on a second. Org link preview. There you go. So now the highlights of your config. I can steal stuff from your config and say, okay, M-j, open parenthesis, oops. M-j. Open parenthesis. I can copy the entire line of LK from avy, which is very nice. Very nice. Yes, yes. So, pretty fast side there into avy. I have to slow down and actually focus on doing the keyboard shortcuts because it's a new habit that I want to build, especially since.

01:02:40 One-shot keyboard modifiers

[Sacha]: Also related to one of your recent videos, I'm experimenting with one-shot keyboard modifiers.

[Prot]: Oh, well done.

[Sacha]: Yes. It's a little tricky. I have to get my brain to get used to it. I'm using keyd to do this on Linux. And it's just getting the hang of pressing control and then moving to the thing. It's messing with my brain a little.

[Prot]: But consider that it's a good opportunity to also use two-handed mode, basically. So, for example, C-x, right? Not like C-x. You see what I'm saying? So basically one hand for the modifier. Yeah, exactly. Because that's a good practice in general, even if you use the standard modifiers. Yeah.

01:03:29 Toggling

[Sacha]: And one of the other things that I started doing after our previous conversation and having looked at some of your toggling sort of things, in your config, what's this idea of using the C-z and C-S-z shortcuts? Since who likes to suspend Emacs anyway, right? So now my C-S-z toggles my now.org, which is the stuff that I'm going to be working on, including the stuff that I want to get the hang of using. So this is my, all right, I need to scope it down so that I don't get overwhelmed. These are the things that will, you know, these are the things that I'm trying to get the hang of using. C-z gets me to my stream notes because then I can add things while I'm live, and then C-S-z is what I have as my now, which also gets posted to my web page, sort of like what I'm focusing on. Which, actually, I can reorganize anyway. So I'm liking this toggling because I can press, like for example, if I'm in the middle of my scratch buffer, I can press C-S-z, pop it up, and then pop it back down. And I was watching Joshua Blais's video about he gets to do this sort of like toggling things in and out from anywhere in his system. So now I'm jealous and I need to figure out how to get that working too.

[Prot]: Yeah, yeah, yeah. That's the kind of thing that is really helpful. Like pop it out and then when you don't need it, it disappears.

01:05:08 System-wide toggle shortcuts using emacsclient

[Sacha]: Do you have any of that kind of system level of toggling even when you don't have Emacs as your main application sort of thing?

[Prot]: Via emacsclient. So you can have a key binding to emacsclient, an emacsclient call, and it will bring up an Emacs window from anywhere. I have that, yes. I have it for a few things. TMR mostly, the timer package. So if I am, for example, here, I can bring it up and start the timer without actually switching to Emacs. Okay,

[Sacha]: so that sounds like something I need to look into. It's

[Prot]: in the prot-window file, prot-window.el. I have a macro there, and it's a macro that defines a command. To run in a new frame and once you do something, such as complete or cancel, to close that frame basically. And it's using a condition

[Sacha]: case. It's using a condition case. I think it's the simplest

[Prot]: you can do.

[Sacha]: And then that's a global keybinding on your window manager that runs that and then brings that so that you can pop it up and put it back.

[Prot]: Yeah. It's just emacsclient -e and then the command.

[Sacha]: Oh, that's interesting. Rickard says using space as control has revolutionized their Emacsing. I'm not sure I'm ready to take that step yet. Also, I can probably figure out how to use keyd to use it as a modifier. We'll see. It's a nice big key, you know? You're just tempted to do all sorts of things with it.

[Prot]: Of course, at the keyboard level, you can have different behavior for tap and hold. So when you tap the space, it's an ordinary space. When you hold it, it's control. Maybe that's what they are.

[Sacha]: Yeah, I think that's what's happening there. Look into using keyd for tap and hold.

[Prot]: Yeah, and this is the principle behind the home row mods, the standard home row mods. It's like when you tap, for example, H, it just does H. When you hold it, it's some modifier key.

01:07:25 My next steps

[Sacha]: I have three minutes before the kiddo runs out and goes, mom, it's lunchtime. So do you have any, like, okay, my next steps, I've got stuff that I need to work on in terms of improving the processing of things and automating things. I found this session very helpful for saying, okay, you know, like, in the weeks leading up to it, two weeks leading up to it, it's like, okay, I got to write this code because I want to be able to say I did it, which is good. And as a result, I have all sorts of fancy things now in my Emacs for streaming and also for my config. In two weeks, I would love to have this kind of conversation with you again, if that's all right with you. Do you have any tips before the kiddo comes out?

01:08:18 Tips from Prot: small functions used frequently

[Prot]: Yeah, yeah, yeah. So for the functions you want to write, you want to make the functions be small so you can test them all and make them part of your habit, like start using them even before the streams. So try to use them every day so that you basically have almost a knee-jerk reaction where it's like, oh, I'm doing this and you call the function basically right away. And I don't know if you use the F keys, the function keys for your shortcuts. Maybe those would be good.

[Sacha]: Yeah, I have some of them. But again, it's hard for me to remember sometimes which one I have matched there. So again, it's trying to build it into muscle memory. Probably what I just need is some kind of drill thing.

01:09:06 Maybe using the header line for tips?

[Prot]: How about a minor mode that sets the header line format? You have seen in many buffers where it says type C-c C-c to finish, right? So set the header line format to be like, you know, type, I don't know, Ctrl-Z to bring up the pop-up, whatever, right?

[Sacha]: Yeah, I mean, quick help sort of is that idea...

[Prot]: Yes, quick help would help you do that as well, yeah.

[Sacha]: It's a screen space thing. But if I can find something that I can smoosh together with keycast so that it reminds me of my key tip in this context. Ah, with keycast. Interesting.

[Prot]: That's why I was thinking of header-line-format. So it would be something that will appear there. And of course, the header line works exactly like the mode line, meaning that it can update the content. It's not static. So like your mode line will update information.

[Sacha]: Yeah. Okay. All right. So let me think about which tips might be, you know, like my keyword shortcut of the day focus could be interesting.

01:10:23 Reorganizing keys

[Prot]: But it also brings the point like here, of course, like the keys you have, maybe it's also a good opportunity to organize them differently. Like the header here should prompt you for one prefix key, for example. Like, you know, C-t, let's say, and that's for transcribing or whatever. Right. And it will just have that one there. And then with the help of which-key, for example, you see what you have behind that prefix.

[Sacha]: I have a hard time figuring out keybindings, which is one of the reasons why I like looking at configs like yours and other people. Because I'm like, yeah, I can totally use that as a starting point for keybindings. But then what else do I assign to it? So for example, I've got this. I apparently don't have this. I have this sacha-stream-transient C-c v. That's where I put it now. Okay. Which now has things like OBS and all that stuff.

[Prot]: What's the mnemonic for v?

[Sacha]: Oh, v would have been video sort of thing.

[Prot]: Okay, I see.

[Sacha]: But I have to fiddle with it and the kiddo is going to come out any moment now. So thanks just in case she comes out.

[Prot]: You're welcome.

[Sacha]: Well, it's lunchtime. Thank you for this. I will schedule something else in two weeks. I'm going to try to practice more scheduled live streams and keep fiddling with this workflow. This has all been very helpful. And thank you to the people who also have dropped by and said hello. You can check the chat later. It's fine. Yes, yes. Thanks, everybody. All right. Okay. I'm going to say bye here just in case. Take care. Take care. Take care, Sacha.

[Prot]: Take care, everybody. Bye-bye. Bye-bye. Thank you.

[Sacha]: Thank you everyone for hanging out. That was my chat with Prot. And I will see y'all again maybe Thurs... Well, probably before then. But I will try to schedule something on Thursday for around that time. Who knows what it's going to be about. But yeah, thank you for coming and experimenting with me. Let us end the stream there. Because it's lunchtime.

View Org source for this post

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

-1:-- YE16: Sacha and Prot talk Emacs (Post Sacha Chua)--L0--C0--2026-04-16T16:44:19.000Z

Irreal: LaTeX Preview In Emacs

Over at the Emacs subreddit, _DonK4rma shows an example of his mathematical note taking in Emacs. It’s a nice example of how flexible Org mode is even for writing text with heavy mathematical content but probably not too interesting to most Emacs users.

What should be interesting is this comment, which points to Dan Davison’s Xenops, which he describes as a “LaTeX editing environment for mathematical documents in Emacs.” The idea is that with Xenops when you leave a math mode block it is automatically rendered as the final mathematics, which replaces the original input. If you move the cursor onto the output text and type return, the original text is redisplayed.

It’s an excellent system that lets you catch any errors you make in entering mathematics as you’re entering them rather than at LaTeX compile time. So far it only works on .tex files but Davison says he will work on getting it to work with Org too.

He has a six minute video that shows the system in action. It gives a good idea of how it works but Xenops can do a lop more; see the repository’s detailed README at the above link for details.

-1:-- LaTeX Preview In Emacs (Post Irreal)--L0--C0--2026-04-16T15:03:07.000Z

Dave Pearson: boxquote.el v2.4

boxquote.el is another of my oldest Emacs Lisp packages. The original code itself was inspired by something I saw on Usenet, and writing my own version of it seemed like a great learning exercise; as noted in the thanks section in the commentary in the source:

Kai Grossjohann for inspiring the idea of boxquote. I wrote this code to mimic the "inclusion quoting" style in his Usenet posts. I could have hassled him for his code but it was far more fun to write it myself.

While I never used this package to quote text I was replying to in Usenet posts, I did use it a lot on Usenet, and in mailing lists, and similar places, to quote stuff.

The default use is to quote a body of text; often a paragraph, or a region, or perhaps even Emacs' idea of a defun.

,----
| `boxquote.el` provides a set of functions for using a text quoting style
| that partially boxes in the left hand side of an area of text, such a
| marking style might be used to show externally included text or example
| code.
`----

Where the package really turned into something fun and enduring, for me, was when I started to add the commands that grabbed information from elsewhere in Emacs and added a title to explain the content of the quote. For example, using boxquote-describe-function to quote the documentation for a function at someone, while also showing them how to get at that documentation:

,----[ C-h f boxquote-text RET ]
| boxquote-text is an autoloaded interactive native-comp-function in
| ‘boxquote.el’.
|
| (boxquote-text TEXT)
|
| Insert TEXT, boxquoted.
`----

Or perhaps getting help with a particular key combination:

,----[ C-h k C-c b ]
| C-c b runs the command boxquote (found in global-map), which is an
| interactive native-comp-function in ‘boxquote.el’.
|
| It is bound to C-c b.
|
| (boxquote)
|
| Show a transient for boxquote commands.
|
|   This function is for interactive use only.
|
| [back]
`----

Or figuring out where a particular command is and how to get at it:

,----[ C-h w fill-paragraph RET ]
| fill-paragraph is on fill-paragraph (M-q)
`----

While I seldom have use for this package these days (mainly because I don't write on Usenet or in mailing lists any more) I did keep carrying it around (always pulling it down from melpa) and had all the various commands bound to some key combination.

(use-package boxquote
  :ensure t
  :bind
  ("<f12> b i"   . boxquote-insert-file)
  ("<f12> b M-w" . boxquote-kill-ring-save)
  ("<f12> b y"   . boxquote-yank)
  ("<f12> b b"   . boxquote-region)
  ("<f12> b t"   . boxquote-title)
  ("<f12> b h f" . boxquote-describe-function)
  ("<f12> b h v" . boxquote-describe-variable)
  ("<f12> b h k" . boxquote-describe-key)
  ("<f12> b h w" . boxquote-where-is)
  ("<f12> b !"   . boxquote-shell-command))

Recently, with the creation of blogmore.el, I moved the boxquote commands off the b prefix (because I wanted that for blogging) and onto an x prefix. Even then... that's a lot of commands bound to a lot of keys that I almost never use but still can't let go of.

Then I got to thinking: I'd made good use of transient in blogmore.el, why not use it here too? So now boxquote.el has acquired a boxquote command which uses transient.

The boxquote transient in action

Now I can have:

(use-package boxquote
  :ensure t
  :bind
  ("C-c b" . boxquote))

and all the commands are still easy to get to and easy to (re)discover. I've also done my best to make them context-sensitive too, so only applicable commands should be usable at any given time.

-1:-- boxquote.el v2.4 (Post Dave Pearson)--L0--C0--2026-04-16T07:29:35.000Z

Bicycle for Your Mind: Outlining with OmniOutliner Pro 6

OmniOutliner Pro 6OmniOutliner Pro 6

Product: OmniOutliner Pro 6
Price: $99 for new users and $50 for upgrade price. They have a $49.99/year subscription price.

Rationale or the Lack of One

There was no good reason to buy OmniOutliner Pro 6.

I don’t need this program. I have the outlining abilities of Org-mode in Emacs. And dedicated outlining programs in Opal, Zavala and TaskPaper.

They had a good upgrade price and I hadn’t tried out any new software in a while. I know that is not a good reason to spend $50. It was my birthday, and I love outlining programs.

I had used the Pro version in version 3 and had bought the Essentials edition for OmniOutliner 5. A lot of what I see in version 6 is new to me.

Themes

Customizing ThemesCustomizing Themes

OmniOutliner Pro 6 comes with themes. I wanted to make my own or customize the existing ones. It is easy to do. Didn’t do much. Changed the line spacing and the font. The themes it ships with are nice. I am using the blank one and Solarized.

Writing Environment

Writing in OOPWriting in OOP

The best thing about OmniOutliner Pro 6 is the writing environment it provides. There are touches around the program which make it a pleasure to write in. Two of them which stick out to me are:

  1. Typewriter scrolling. I have no idea why more programs don’t give you this feature. I use it all the time. Looking at the bottom of the document is boring and it hurts my neck.
  2. Full screen focus. This is well implemented and another feature which helps me concentrate on the document I am in.

Linking Documents

LinkingLinking

You can link to a document or to a block in the document. Clicking on the space left of the Heading gives you a drop-down menu. Choose the Copy Omni Link and paste it to where you want the link to appear. Useful in linking documents or sections when you have a block of outlines which relate to each other in some way.

Keyboard Commands

keyboard commandskeyboard commands

Keyboard commands are what make an outlining program. OmniOutliner Pro 6 comes with the ability to customize and change every keyboard command that is in the program. It makes the learning curve smoother when you can use the commands you are used to for every task you perform in an outliner. I love this ability to make the outliner my own.

Using OmniOutliner Pro 6

This is the best outliner in the macOS space. OmniOutliner Pro 6 cements that position. It is a pleasure to use. It does everything you need from an outliner and does it with style. It does more than you need. Columns? I have never found the need for columns in an outliner. Other users love this feature. I am not interested. Maybe I am missing something, or I don’t use outlines which need columns. In spite of my lack of enthusiasm for columns, this is the best outlining program available on the macOS.

Comparison with Org-mode

I use Emacs and within it Org-mode. I write in outlines in Emacs all the time.

Org-mode is a strange mix of OmniOutliner and OmniFocus. It does outlines and does task management. All in one application. In plain text. The only problem? You have to deal with the complexity of Emacs. It is a steep learning curve which gives you benefits over the long term but there is pain in the short term. Let’s be honest, there is a ton of pain in the short term. OmniOutliner on the other hand, is easy to pick up and use. You are going to be competent in the program with little effort. The learning curve is minimal. The program is usable and useful. Doesn’t do most of the things Org-mode does, but it is not designed for that. They have a product called OmniFocus to sell you, for that.

Conclusion

If you are looking for an outlining program, you cannot go wrong with OmniOutliner Pro 6. It is fantastic to live in and work with. It gives you a great writing environment. I love writing in it.

There are two things which give me pause when it comes to OmniOutliner Pro 6. The first is the price. I think $99 for an outlining program is steep. That is a function of my retired-person price sensitivity. You might have a different view. The second is the incomplete documentation. They are working on it, slowly. If I am paying for the most expensive outlining program in the marketplace, I want the documentation to be complete and readily available on sale of the product. Not something I have been waiting a few months for. That is negligent.

If you are looking at outlining programs there are competitors in the marketplace. Zavala is a competitive product which is free. Opal is another product which is free and although it doesn’t have all the features of OmniOutliner, is a competent outliner. Or, you can always learn how to use Emacs and adopt Org-mode as the main driver of all your writing.

OmniOutliner Pro 6 is recommended with some reservations.

macosxguru at the gmail thingie.

-1:-- Outlining with OmniOutliner Pro 6 (Post Bicycle for Your Mind)--L0--C0--2026-04-16T07:00:00.000Z

James Endres Howell: Embedding a Mastodon thread as comments to a blog post

I wrote org-static-blog-emfed, a little Emacs package that extends org-static-blog with the ability to embed a Mastodon thread in a blog post to serve as comments. The root of the Mastodon thread also serves as an announcement of the blog post to your followers. It’s based on Adrian Sampson’s Emfed, and of course Bastian Bechtold’s org-static-blog.

I had shared it before, but alas, after changing Mastodon instances the comments from old posts were lost, so I disabled them on this blog. Just over the past few days I’ve found time to get it all working again.

It also seems, at least in #Emacs on Mastodon, that org-static-blog has gained in popularity recently.

Prompted as I was to make a few improvements, I thought I would update the README and share it again. Hope it’s useful for someone!

-1:-- Embedding a Mastodon thread as comments to a blog post (Post James Endres Howell)--L0--C0--2026-04-15T22:17:00.000Z

James Dyer: Emacs-DIYer: A Built-in dired-collapse Replacement

I have been slowly chipping away at my Emacs-DIYer project, which is basically my ongoing experiment in rebuilding popular Emacs packages using only what ships with Emacs itself, no external dependencies, no MELPA, just the built-in pieces bolted together in a literate README.org that tangles to init.el. The latest addition is a DIY version of dired-collapse from the dired-hacks family, which is one of those packages I did not realise I leaned on until I started browsing a deeply-nested Java project and felt the absence immediately.

If you have ever opened a dired buffer on something like a Maven project, or node_modules, or a freshly generated resource bundle, you will know the pain, src/ contains a single main/ which contains a single java/ which contains a single com/ which contains a single example/, and you are pressing RET four times just to get to anything interesting. The dired-collapse minor mode from dired-hacks solves this beautifully, it squashes that whole single-child chain into one dired line so src/main/java/com/example/ shows up as a single row and one RET drops you straight into the deepest directory.

So, as always with the Emacs-DIYer project, I wondered, can I implement this in a few elisp defuns?

Right, so what is the plan?, dired already draws a nice listing with permissions, sizes, dates and filenames, all I really need to do is walk each line, look at the directory, figure out the deepest single-child descendant, and then rewrite the filename column in place with the collapsed path. The trick, and this is the bit that took me a minute to convince myself of, is that dired uses a dired-filename text property to know where the filename lives on the line, and dired-get-filename happily accepts relative paths containing slashes. So if I can rewrite the text and reapply the property, everything else, RET, marking, copying, should just work without me having to touch the rest of dired at all!

First function, my/dired-collapse--deepest, which just walks the directory chain as long as each directory contains exactly one accessible child directory. I added a 100-iteration guard so a pathological symlink cycle cannot wedge the whole thing, which, you know, future me might thank present me for:

(defun my/dired-collapse--deepest (dir)
"Return the deepest single-child descendant directory of DIR.
Walks the directory chain as long as each directory contains exactly
one entry which is itself an accessible directory. Stops after 100
iterations to guard against symlink cycles."
(let ((current dir)
(depth 0))
(catch 'done
(while (< depth 100)
(let ((entries (condition-case nil
(directory-files current t
directory-files-no-dot-files-regexp
t)
(error nil))))
(if (and entries
(null (cdr entries))
(file-directory-p (car entries))
(file-accessible-directory-p (car entries)))
(setq current (car entries)
depth (1+ depth))
(throw 'done current)))))
current))

directory-files-no-dot-files-regexp is one of those lovely little built-in constants I keep forgetting exists, it filters out . and .. but keeps dotfiles, which is exactly what you want if you are deciding whether a directory is truly single-child.

Second function does the actual buffer surgery, my/dired-collapse iterates each dired line, grabs the filename with dired-get-filename, asks the walker how deep the chain goes, and if there is anything to collapse it replaces the displayed filename with the collapsed relative path:

(defun my/dired-collapse ()
"Collapse single-child directory chains in the current dired buffer.
A DIY replacement for `dired-collapse-mode' from the dired-hacks
package. Rewrites the filename portion of each line in place and
reapplies the `dired-filename' text property so that standard dired
navigation still resolves to the deepest directory."
(when (derived-mode-p 'dired-mode)
(let ((inhibit-read-only t))
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(condition-case nil
(let ((file (dired-get-filename nil t)))
(when (and file
(file-directory-p file)
(not (member (file-name-nondirectory
(directory-file-name file))
'("." "..")))
(file-accessible-directory-p file))
(let ((deepest (my/dired-collapse--deepest file)))
(unless (string= deepest file)
(when (dired-move-to-filename)
(let* ((start (point))
(end (dired-move-to-end-of-filename t))
(displayed (buffer-substring-no-properties
start end))
(suffix (substring deepest
(1+ (length file))))
(new (concat displayed "/" suffix)))
(delete-region start end)
(goto-char start)
(insert (propertize new
'face 'dired-directory
'mouse-face 'highlight
'dired-filename t))))))))
(error nil))
(forward-line))))))

The key bit is the propertize call at the end, the new filename text has to carry dired-filename t so that dired-get-filename picks it up, and dired-directory on face keeps the collapsed entry looking the same as a normal directory line. Because dired-get-filename will happily glue a relative path like main/java/com/example onto the dired buffer’s directory, pressing RET on a collapsed line takes you straight to src/main/java/com/example with no extra work from me.

A while back I added a little unicode icon overlay thing to dired (my/dired-add-icons, which puts a little symbol in front of each filename via a zero-length overlay), and I did not want the collapse to fight with it. The icons hook into dired-after-readin-hook as well, so I just gave collapse a negative depth when attaching its hook:

(add-hook 'dired-after-readin-hook #'my/dired-collapse -50)

Lower depth runs earlier, so collapse rewrites the line first, then the icon overlay attaches to the final collapsed filename position. Without this, the icons would happily sit in front of a stub directory that was about to be rewritten, which is, well, fine I suppose, but it felt tidier to have them anchor on the post-collapse text.

Before, a typical Maven project root might look something like this:

drwxr-xr-x 3 jdyer users 4096 Apr 9 08:12 ▶ src
drwxr-xr-x 2 jdyer users 4096 Apr 9 08:11 ▶ target
-rw-r--r-- 1 jdyer users 812 Apr 9 08:10 ◦ pom.xml

After collapse kicks in:

drwxr-xr-x 3 jdyer users 4096 Apr 9 08:12 ▶ src/main/java/com/example
drwxr-xr-x 2 jdyer users 4096 Apr 9 08:11 ▶ target
-rw-r--r-- 1 jdyer users 812 Apr 9 08:10 ◦ pom.xml

One RET and you are in com/example, which is where all the actual code lives anyway. Marking, copying, deleting, renaming, all of it still behaves because the dired-filename text property points at the real deepest path.

One thing that initially bit me, is navigating out of a collapsed chain. If I hit RET on a collapsed src/main/java/com/example line I land in the deepest directory, which is great, but then pressing my usual M-e to go back up was doing the wrong thing. M-e in my config has always been bound to dired-jump, and dired-jump called from inside a dired buffer does a “pop up a level” thing that ended up spawning a fresh dired for com/, bypassing the collapsed view entirely and leaving me staring at a directory I never wanted to see.

My first attempt at fixing this was to put some around-advice on dired-jump so that if an existing dired buffer already had a collapsed line covering the jump target, it would switch to that buffer and land on the collapsed line instead of splicing in a duplicate subdir. It worked, sort of, but dired-jump in general felt a bit janky inside dired, it does a lot of “refresh the buffer and try again” under the hood and the in-dired pop-up-a-level path was always the weak link. So I stepped back and split the two cases apart with a tiny dispatch wrapper:

(defun my/dired-jump-or-up ()
"If in Dired, go up a directory; otherwise dired-jump for current buffer."
(interactive)
(if (derived-mode-p 'dired-mode)
(dired-up-directory)
(dired-jump)))
(global-set-key (kbd "M-e") #'my/dired-jump-or-up)

From a file buffer, dired-jump is still exactly the right thing as you want the directory the file is in of course. From inside a dired buffer, dired-up-directory is just a much cleaner operation, it walks up one real level, no refresh, no splicing, nothing weird. But on its own that would lose the collapsed round-trip, so I gave dired-up-directory its own bit of advice that looks for a collapsed-ancestor buffer before falling through to the default behaviour.

(defun my/dired-collapse--find-hit (target-dir)
"Return (BUFFER . POS) of a dired buffer with a collapsed line covering TARGET-DIR."
(let ((target (file-name-as-directory (expand-file-name target-dir)))
hit)
(dolist (buf (buffer-list))
(unless hit
(with-current-buffer buf
(when (and (derived-mode-p 'dired-mode)
(stringp default-directory))
(let ((buf-dir (file-name-as-directory
(expand-file-name default-directory))))
(when (and (string-prefix-p buf-dir target)
(not (string= buf-dir target)))
(save-excursion
(goto-char (point-min))
(catch 'found
(while (not (eobp))
(let ((line-file (ignore-errors
(dired-get-filename nil t))))
(when (and line-file
(file-directory-p line-file))
(let ((line-dir (file-name-as-directory
(expand-file-name line-file))))
(when (string-prefix-p target line-dir)
(setq hit (cons buf (point)))
(throw 'found nil)))))
(forward-line))))))))))
hit))

The dired-up-directory only fires when the literal parent is not already open as a dired buffer, which keeps normal upward navigation completely unchanged:

(defun my/dired-collapse--up-advice (orig-fn &optional other-window)
"Around-advice for `dired-up-directory' restoring collapsed round-trip."
(let* ((dir (and (derived-mode-p 'dired-mode)
(stringp default-directory)
(expand-file-name default-directory)))
(up (and dir (file-name-directory (directory-file-name dir))))
(parent-buf (and up (dired-find-buffer-nocreate up)))
(hit (and dir (null parent-buf)
(my/dired-collapse--find-hit dir))))
(if hit
(let ((buf (car hit))
(pos (cdr hit)))
(if other-window
(switch-to-buffer-other-window buf)
(pop-to-buffer-same-window buf))
(goto-char pos)
(dired-move-to-filename))
(funcall orig-fn other-window))))
(advice-add 'dired-up-directory :around #'my/dired-collapse--up-advice)

If /proj/src/main/java/com/ happens to already exist as a dired buffer, dired-up-directory does its usual thing and just goes there, the up-advice never fires. It is only when the literal parent is absent that the advice kicks in and hands you back to the collapsed ancestor, which I think is the right tradeoff, the advice never surprises you when you were going to get the standard behaviour anyway, it only steps in when the standard behaviour would throw away context you clearly still had in a buffer somewhere.

End result, RET into a collapsed chain drops me deep, M-e walks me back out to the original collapsed line, and none of it requires doing anything clever with dired-jump’s “pop up a level” path, which I am increasingly convinced I should not have been using in the first place.

Everything lives in the Emacs-DIYer project on GitHub, in the literate README.org. If you just want the snippet to drop into your own init file, the two functions and the add-hook line above are the whole thing, no require, no use-package, no MELPA, just built-in dired and a bit of buffer shenanigans, and thats it!, phew, and breathe!

-1:-- Emacs-DIYer: A Built-in dired-collapse Replacement (Post James Dyer)--L0--C0--2026-04-15T18:20:00.000Z

Dave Pearson: slstats.el v1.11

Yet another older Emacs Lisp package that has had a tidy up. This one is slstats.el, a wee package that can be used to look up various statistics about the Second Life grid. It's mainly a wrapper around the API provided by the Second Life grid survey.

When slstats is run, you get an overview of all of the information available.

An overview of the grid

There are also various commands for viewing individual details about the grid in the echo area:

  • slstats-signups - Display the Second Life sign-up count
  • slstats-exchange-rate - Display the L$ -> $ exchange rate
  • slstats-inworld - Display how many avatars are in-world in Second Life
  • slstats-concurrency - Display the latest-known concurrency stats for Second Life
  • slstats-grid-size - Display the grid size data for Second Life

There is also slstats-region-info which will show information and the object and terrain maps for a specific region.

Region information for Da Boom

As with a good few of my older packages: it's probably not that useful, but at the same time it was educational to write it to start with, and it can be an amusement from time to time.

-1:-- slstats.el v1.11 (Post Dave Pearson)--L0--C0--2026-04-15T14:52:55.000Z

Irreal: Switching Between Dired Windows With TAB

Just a quickie today. Marcin Borkowski (mbork) has a very nice little post on using Tab with Dired. By default, Tab isn’t defined in Dired but mbork suggests an excellent use for it and provides the code to implement his suggestion.

If there are two Dired windows open, the default destination for Dired commands is “the other window”. That’s a handy thing that not every Emacs user knows. Mbork’s idea is to use Tab to switch between Dired windows.

It’s a small thing, of course, but it’s a nice example of reducing friction in your Emacs workflow. As Mbork says, it’s yet another example of how easy it is to make small optimizations like this in Emacs.

Update [2026-04-16 Thu 11:06]: Added link to mbork’s post.

-1:-- Switching Between Dired Windows With TAB (Post Irreal)--L0--C0--2026-04-15T14:42:10.000Z

Gal Buki: Clipboard in terminal Emacs with WezTerm

Although TRAMP allows access to files on remote servers using the local Emacs instance I usually prefer to open Emacs using a running daemon session on the remote server.

The issue with Emacs in the terminal is that kill and yank (aka copy and paste) don't work the same way as with the GUI. Using WezTerm I have found that it is

SSH clipboard support

My terminal emulator of choice is WezTerm which already supports bidirectional kill & yank out of the box.

But I can't bring my muscle memory to remember to use Ctrl+Shift+V to yank text in Emacs. I want Ctrl+y​/​C-y, like I'm used to.

Luckily .wezterm.lua lets us catch Ctrl+y and yank the clipboard contents into the terminal and with that into Emacs.

local wezterm = require 'wezterm'

local config = wezterm.config_builder()

config.keys = {
    -- Paste in Emacs using regular key bindings
    {
      key = "y",
      mods = "CTRL",
      action = wezterm.action.PasteFrom "Clipboard",
    },
}

return config

Local clipboard support

For those wanting to run Emacs in a local terminal WezTerm provides yank out of the box but not kill. To kill text from Emacs into the local clipboard we need to use xclip.

The xclip package has an auto-detect function but it has some issues.

  • if it finds xclip or xsel it will use them even if we are on Wayland
  • it can't detect MacOS (darwin)

So I decided to set the xclip-method manually. In addition I use the :if option of use-package to limit loading the package only when we are in the terminal, an xclip-method was found and we aren't using ssh.

(defun tjkl/xclip-method ()
  (cond
   ((eq system-type 'darwin) 'pbpaste)
   ((getenv "WAYLAND_DISPLAY") 'wl-copy)
   ((getenv "DISPLAY") 'xsel)
   ((getenv "WSLENV") 'powershell)
   (t nil)))

(use-package xclip
  :if (and (not (display-graphic-p))
           (not (getenv "SSH_CONNECTION"))
           (tjkl/xclip-method))
  :custom
  (xclip-method (tjkl/xclip-method))
  :config
  (xclip-mode 1))

Local clipboard without xclip

It is possible to use OSC-52 (Output/Escape Sequences) in a local WezTerm terminal without the xclip package and cli tool.
The problem with this approach is that we can't work with terminal and GUI Emacs using the same session. Since interprogram-cut-function is global it will also try to use OSC52 in the GUI Emacs and fail with the message progn: Device 1 is not a termcap terminal device.

I have not yet found a good way to restore GUI yank functionality once interprogram-cut-function is set. So the following should only be used if the GUI instance doesn't use the same session or if the GUI is never opened after terminal Emacs.

(unless (display-graphic-p)
  (defun tjkl/osc52-kill (text)
    (when (and text (stringp text))
      (send-string-to-terminal
       (format "\e]52;c;%s\a"
               (base64-encode-string text t)))))
  (setq interprogram-cut-function #'tjkl/osc52-kill))
-1:-- Clipboard in terminal Emacs with WezTerm (Post Gal Buki)--L0--C0--2026-04-15T10:50:00.000Z

Sacha Chua: Org Mode: JS for translating times to people's local timezones

I want to get back into the swing of doing Emacs Chats again, which means scheduling, which means timezones. Let's see first if anyone happens to match up with the Thursday timeslots (10:30 or 12:45) that I'd like to use for Emacs-y video things, but I might be able to shuffle things around if needed.

I want something that can translate times into people's local timezones. I use Org Mode timestamps a lot because they're so easy to insert with C-u C-c ! (org-timestamp-inactive), which inserts a timestamp like this:

By default, the Org HTML export for it does not include the timezone offset. That's easily fixed by adding %z to the time specifier, like this:

(setq org-html-datetime-formats '("%F" . "%FT%T%z"))

Now a little bit of Javascript code makes it clickable and lets us toggle a translated time. I put the time afterwards so that people can verify it visually. I never quite trust myself when it comes to timezone translations.

function translateTime(event) {
  if (event.target.getAttribute('datetime')?.match(/[0-9][0-9][0-9][0-9]$/)) {
    if (event.target.querySelector('.translated')) {
      event.target.querySelectorAll('.translated').forEach((o) => o.remove());
    } else {
      const span = document.createElement('span');
      span.classList.add('translated');
      span.textContent = ' → ' + (new Date(event.target.getAttribute('datetime'))).toLocaleString(undefined, {
        month: 'short',  
        day: 'numeric',  
        hour: 'numeric', 
        minute: '2-digit',
        timeZoneName: 'short'
      });
      event.target.appendChild(span);
    }
  }
}
function clickForLocalTime() {
  document.querySelectorAll('time').forEach((o) => {
    if (o.getAttribute('datetime')?.match(/[0-9][0-9][0-9][0-9]$/)) {
      o.addEventListener('click', translateTime);
      o.classList.add('clickable');
    }
  });
}

And some CSS to make it more obvious that it's now clickable:

.clickable {
    cursor: pointer;
    text-decoration: underline dotted;
}

Let's see if this is useful.

Someday, it would probably be handy to have a button that translates all the timestamps in a table, but this is a good starting point.

View Org source for this post

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

-1:-- Org Mode: JS for translating times to people's local timezones (Post Sacha Chua)--L0--C0--2026-04-14T18:44:16.000Z

Irreal: Alfred Snippets

Today while I was going through my feed, I saw this this post from macosxguru over at Bicycle For Your Mind. It’s about his system for using snippets on his system. The TL;DR is that he has settled on Typinator and likes it a lot.

I use snippets a lot but use several systems—YASnippet, abbrev mode, and the macOS text expansion facility—but none of them work everywhere I need them to so I have to negotiate three different systems. YASnippet is different from the other two in that its snippets can accept input instead of just making a text substation like the others.

In his post, macosxguru mentions that his previous system for text substitutions was based on the Alfred snippet functions. I’ve been using Alfred for a long time and love it. A one time purchase of the power pack makes your Mac much more powerful. Still, even though I was vaguely aware of it, I’d never used Alfred’s snippet function.

After seeing it mentioned on macosxguru’s post I decided to try it out. It’s easy to specify text substitutions. I couldn’t immediately figure out how to trigger the substitutions manually so I just set them to trigger automatically. I usually don’t like that but so far it’s working out well.

Up til now, I haven’t found anywhere that the substitutions don’t work. That can’t be said of any of the other systems I was using. It’s particularly hard to find one that works with both Emacs and other macOS applications.

If you’re using Emacs on macOS, you should definitely look into Alfred. It plays very nicely with Emacs and my newfound snippets ability makes the combination even better.

-1:-- Alfred Snippets (Post Irreal)--L0--C0--2026-04-14T14:59:57.000Z

Dave Pearson: wordcloud.el v1.4

I think I'm mostly caught up with the collection of Emacs Lisp packages that need updating and tidying, which means yesterday evening's clean-up should be one of the last (although I would like to revisit a couple and actually improve and extend them at some point).

As for what I cleaned up yesterday: wordcloud.el. This is a package that, when run in a buffer, will count the frequency of words in that buffer and show the results in a fresh window, complete with the "word cloud" differing-font-size effect.

Word cloud in action

This package is about 10 years old at this point, and I'm struggling to remember why I wrote it now. I know I was doing something -- either writing something or reviewing it -- and the frequency of some words was important. I also remember this doing the job just fine and solving the problem I needed to solve.

Since then it's just sat around in my personal library of stuff I've written in Emacs Lisp, not really used. I imagine that's where it's going back to, but at least it's cleaned up and should be functional for a long time to come.

-1:-- wordcloud.el v1.4 (Post Dave Pearson)--L0--C0--2026-04-14T07:47:39.000Z

Dave's blog: Posframe for everything

An Emacser recently posted about popterm, which can use posframe to toggle a terminal visible and invisible in Emacs. I tried it out, and ran into problems with it, so abandoned it for now.

However, this got me thinking about other things that can use posframe, which pops up a frame at point. I’ve seen other Emacsers use posframe when they show off their configurations in meetups. I thought about what I use often that might benefit from a posframe.

  • magit
  • vertico
  • which-key
  • company
  • flymake

Which of these has something I can use to enable posframes?

Of course, there are plenty of other packages that have add-on packages to enable posframes.

Magit

magit doesn’t have anything directly, but it makes heavy use of transient. And there’s a package transient-posframe that can enable posframes for transients. When I use magit’s transients, the transient pops up as a frame in the middle of my Emacs frame.

vertico

Install vertico-posframe to use posframes with vertico.

which-key

Yep, there’s which-key-posframe.

company

See company-posframe.

flymake

I needed a bit of web searching to find this. flymake-popon can use a posframe in the GUI and popon in a terminal.

-1:-- Posframe for everything (Post Dave's blog)--L0--C0--2026-04-14T00:00:00.000Z

Marcin Borkowski: Binding TAB in Dired to something useful

I’m old enough to remember Norton Commander for DOS. Despite that, I never used Midnight Commander nor Sunrise Commander – Dired is still my go-to file manager these days. In fact, Dired has a feature which seems to be inspired by NC: when there are two Dired windows, the default destination for copying, moving and symlinking is “the other” window. Surprisingly, another feature which would be natural in an orthodox file manager is absent from Dired
-1:-- Binding TAB in Dired to something useful (Post Marcin Borkowski)--L0--C0--2026-04-13T18:56:07.000Z

Irreal: Some Config Hacks

Bozhidar Batsov has an excellent post that collects several configuration hacks from a variety of people and distributions. It’s a long list and rather than list them all, I’m going to mention just a few that appeal to me. Some of them I’m already using. Other’s I didn’t know about but will probably adopt.

  • Save the clipboard before killing: I’ve been using this for years. What it does is to make sure that the contents of the system clipboard aren’t lost if you do a kill in Emacs. This is much more useful than it sounds, especially if, like me, your do a lot of cutting and pasting from other applications.
  • Save the kill ring across sessions: I’m not sure I’ll adopt this but it’s easy to see how it could be useful.
  • Auto-Chmod spripts: Every time I see this one I resolve to add it to my config but always forget. What it does is automatically make scripts (files beginning with #!) executable when they’re saved.
  • Proportional window resizing: When a window is split, this causes all the windows in the frame to resize proportionally.
  • Faster mark popping. It’s sort of like repeat mode for popping the mark ring. After the first Ctrl+u Ctrl+Space you can continue popping the ring with a simple Ctrl+Space
  • Auto-select Help window: This is my favorite.When I invoke help, I almost always want to interact with the Help buffer if only to quit and delete it with a q. Unfortunately, the Help buffer doesn’t get focus so I have to do a change window to it. This simple configuration gives the Help buffer focus when you open it.

Everybody’s needs and preferences are different, of course, so be sure to take a look at Bastov’s post to see which ones might be helpful to you.

-1:-- Some Config Hacks (Post Irreal)--L0--C0--2026-04-13T14:56:38.000Z

Sacha Chua: 2026-04-13 Emacs news

Lots of little improvements in this one! I'm looking forward to borrowing the config tweaks that bbatsov highlighted and also trying out popterm for quick-access shells. Also, the Emacs Carnival for April has a temporary home at Newbies/starter kits - feel free to write and share your thoughts!

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

View Org source for this post

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

-1:-- 2026-04-13 Emacs news (Post Sacha Chua)--L0--C0--2026-04-13T13:43:00.000Z

Protesilaos Stavrou: Emacs: new modus-themes-exporter package live today @ 15:00 Europe/Athens

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

UPDATE 2026-04-13 18:00 +0300: I wrote the package during the stream: https://github.com/protesilaos/modus-themes-exporter.


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

Today, the 13th of April 2026, at 15:00 Europe/Athens I will do a live stream in which I will develop the new modus-themes-exporter package for Emacs.

The idea for this package is based on an old experiment of mine: to get the palette of a Modus theme and “export” it to another file format for use in supported terminal emulators or, potentially, other applications.

My focus today will be on writing the core functionality and testing it with at least one target application.

Prior work of mine from my pre-Emacs days is the tempus-themes-generator, which was written in Bash: https://gitlab.com/protesilaos/tempus-themes-generator.

-1:-- Emacs: new modus-themes-exporter package live today @ 15:00 Europe/Athens (Post Protesilaos Stavrou)--L0--C0--2026-04-13T00:00:00.000Z

Irreal: Days Until

Charles Choi recently saw a Mastodon post showing the days until the next election and started wondering how one would compute that with Emacs. He looked into it and, of course, the answer turned out to be simple. Org mode has a function, org-time-stamp-to-now that does exactly that. It takes a date string and calculates the number of days until that date.

Choi wrote an internal function that takes a date string and outputs a string specifying the number of days until that date. The default is x days until <date string> but you can specify a different output string if you like. That function, cc/--days-until, serves as a base for other functions.

Choi shows two such functions. One that allows you to specify a date from a date picker and computes the number of days until that date. The other—following the original question—computers the number of days until the next midterm and general elections in the U.S. for 2006. It’s a simple matter to change it for other election years. Nobody but the terminally politically obsessed would care about that but it’s a nice example of how easy it is to use cc/--days-until to find the number of days until some event.

Finally, in the comments to Choi’s reddit announcement ggxx-sdf notes that you can also use calc-eval for these sorts of calculations.

As Choi says, it’s a human characteristic to want to know how long something is going to take. If you have some event that you want a countdown clock for, take a look at Choi’s post.

-1:-- Days Until (Post Irreal)--L0--C0--2026-04-12T14:50:16.000Z

Bicycle for Your Mind: Expanding with Typinator 10

TypinatorTypinator

Product: Typinator
Price: $49.99 (one time for macOS only) or $29.99/yearly (for macOS and iOS version)

I was a TextExpander user and switched from it to aText when TextExpander went to a subscription model. Been using Alfred for snippet expansions for well over… Actually I have no idea how long. Every since Alfred added that feature I suppose. There are expansions which require input, and those are handled by Keyboard Maestro. I wanted to see what was available in this space. There was no good reason for the change, I was perfectly happy with the setup. But I saw that Typinator 10 had been released and I got curious. Approached the developer and they were kind enough to provide me with a license. So, this is the review.

What Does a Text Expansion Program Do?

A text expansion program makes it easy to type content you use regularly. For instance, I have an expansion where I type ,bfym and [Bicycle For Your Mind](http://bicycleforyourmind.com) is pasted into the text. It lessens your typing load, stops you from making mistakes and makes typing easy. Expansions include corrections of common mistakes that you or other people make while typing. It includes emojis and symbols. It can be simple or complex depending on your needs.

macOS has a built in mode for text expansions, but it is limited and like a lot of things macOS does, they include it without giving it much attention or developer love. It is lacking in features or finesse. If you are serious about making your writing comfortable and easy, you need to consider third party solutions. The macOS marketplace has a fair number of programs which tackle this task. The two main products are TextExpander and Typinator. Both Alfred and Keyboard Maestro have this feature built into the program.

Typinator 1Typinator 1

iOS

The main feature in this version of Typinator is the iOS integration. I am not interested in that, I am not going to talk about that. As far as I know, TextExpander was the only other product which had that integration. Typinator is now matching them. For some people, this is a crucial feature. Going by my experience with this developer, I am sure Typinator works as well on iOS.

Surprises

Typinator lets me use regex to define expansions. One of the ones which gets used all the time lets me type a period and then the first letter of the next sentence gets capitalized automatically. You have no idea how much I like that. Apple has that as a setting but it is temperamental. Not Typinator. Works like a charm. Thanks to its regex support it does interesting things with dates. I love that feature although I haven’t used it enough to make it super useful. I see the potential there.

Observations

Converting my Alfred snippets to Typinator was easy. Save the snippets in Alfred as a CSV file and then import those into Typinator.

Typinator keeps a record of the number of times you use a particular expansion and the last time you used it. Gives me the ability to monitor the usage of the expansions. Alfred doesn’t do that. I use abbrev.mode in Emacs, and that keeps a running count too. I love that feature.

Typinator 2Typinator 2

Typinator is easy to interact with. It has a menu-bar icon which you can click on to get the main window or you can assign a system wide keyboard command to bring the window up. You have the ability to highlight something in any editor you are using and press a keyboard command to bring up a dialog box to set up an expansion based on the content you have highlighted. Easy. I find myself using this to increase the number of expansions I have available.

Typinator gives you minute control over the expansions. You have the ability to trigger the expansions immediately upon matching the expansion trigger or after a word break. In other words, you can expand as soon as you match or expand after you type a space or any punctuation after your match. This setting is available on every individual snippet. Every individual snippet can be set for ignoring case or expand on exact match. Another level of fine control which is useful.

This is a mature program. It has been available for a long while now. It is a full-featured expansion program. They have been at it for a while and they are good at it.

Conclusion

If you are looking for a text expansion program, you cannot go wrong with Typinator. It is great at what it does and is full of features which will make you smile. I love it.

I recommend Typinator with enthusiasm.

macosxguru at the gmail thingie.

-1:-- Expanding with Typinator 10 (Post Bicycle for Your Mind)--L0--C0--2026-04-12T07:00:00.000Z

Tim Heaney: Computing Days Until with Perl and Rust

The other day Charles Choi wrote about Computing Days Until with Emacs. I decided to try it in Perl and Rust. Perl In Perl, we could do it with just the standard library like so. #!/usr/bin/env perl use v5.42; use POSIX qw(ceil); use Time::Piece; use Time::Seconds; my $target_date = shift // die "\nUsage: $0 YYYY-MM-DD\n"; my $target = Time::Piece->strptime($target_date, "%Y-%m-%d"); my $today = localtime; my $delta = $target - $today; say ceil $delta->days; Subtracting two Time::Piece objects gives a Time::Seconds object, which has a days method.
-1:-- Computing Days Until with Perl and Rust (Post Tim Heaney)--L0--C0--2026-04-12T00:00:00.000Z

Irreal: Magit Support

Just about everyone agrees that the two Emacs packages considered “killer apps” by those considering adopting the editor are Org mode and Magit. I’ve seen several people say they use Emacs mainly for one or the other.

Their development models are completely different. Org has a development team with a lead developer in much the same way that Emacs does. Magit is basically a one man show, although there are plenty of contributors offering pull requests and even fixing bugs. That one man is Jonas Bernoulli (tarsius) who develops Magit full time and earns his living from doing so.

Like most nerds, he hates marketing and would rather be writing code than seeking funding. Still, that thing about earning a living from Magit means that he must occasionally worry about raising money. Now is one such time. Some of his funding pledges have expired and the weakening U.S. dollar is also contributing to his dwindling income.

Virtually every Emacs user is also a Magit user and many of us depend on it so now would be a propitious moment to chip in some money to keep the good times rolling. The best thing, of course, is to get your employer to make a more robust contribution than would be feasible for an individual developer but even if every developer chips in a few dollars (or whatever) we can support tarsius and allow him to continue working on Magit and its associated packages.

His support page is here. Please consider contributing a few dollars. Tarsius certainly deserves it and we’ll be getting our money’s worth.

-1:-- Magit Support (Post Irreal)--L0--C0--2026-04-11T14:24:14.000Z

Sacha Chua: Org Mode: Tangle Emacs config snippets to different files and add boilerplate

I want to organize the functions in my Emacs configuration so that they are easier for me to test and so that other people can load them from my repository. Instead of copying multiple code blogs from my blog posts or my exported Emacs configuration, it would be great if people could just include a file from the repository. I don't think people copy that much from my config, but it might still be worth making it easier for people to borrow interesting functions. It would be great to have libraries of functions that people can evaluate without worrying about side effects, and then they can copy or write a shorter piece of code to use those functions.

In Prot's configuration (The custom libraries of my configuration), he includes each library as in full, in a single code block, with the boilerplate description, keywords, and (provide '...) that make them more like other libraries in Emacs.

I'm not quite sure my little functions are at that point yet. For now, I like the way that the functions are embedded in the blog posts and notes that explain them, and the org-babel :comments argument can insert links back to the sections of my configuration that I can open with org-open-at-point-global or org-babel-tangle-jump-to-org.

Thinking through the options...

Org tangles blocks in order, so if I want boilerplate or if I want to add require statements, I need to have a section near the beginning of my config that sets those up for each file. Noweb references might help me with common text like the license. Likewise, if I want a (provide ...) line at the end of each file, I need a section near the end of the file.

If I want to specify things out of sequence, I could use Noweb. By setting :noweb-ref some-id :tangle no on the blocks I want to collect later, I can then tangle them in the middle of the boilerplate. Here's a brief demo:

#+begin_src emacs-lisp :noweb yes :tangle lisp/sacha-eshell.el :comments no
;; -*- lexical-binding: t; -*-
<<sacha-eshell>>
(provide 'sacha-eshell)
#+end_src

However, I'll lose the comment links that let me jump back to the part of the Org file with the original source block. This means that if I use find-function to jump to the definition of a function and then I want to find the outline section related to it, I have to use a function that checks if this might be my custom code and then looks in my config for "defun …". It's a little less generic.

I wonder if I can combine multiple targets with some code that knows what it's being tangled to, so it can write slightly different text. org-babel-tangle-single-block currently calculates the result once and then adds it to the list for each filename, so that doesn't seem likely.

Alternatively, maybe I can use noweb or my own tangling function and add the link comments from org-babel-tangle-comments.

Aha, I can fiddle with org-babel-post-tangle-hook to insert the boilerplate after the blocks have been written. Then I can add the lexical-binding: t cookie and the structure that makes it look more like the other libraries people define and use. It's always nice when I can get away with a small change that uses an existing hook. For good measure, let's even include a list of links to the sections of my config that affect that file.

(defvar sacha-dotemacs-url "https://sachachua.com/dotemacs/")

;;;###autoload
(defun sacha-dotemacs-link-for-section-at-point (&optional combined)
  "Return the link for the current section."
  (let* ((custom-id (org-entry-get-with-inheritance "CUSTOM_ID"))
         (title (org-entry-get (point) "ITEM"))
         (url (if custom-id
                  (concat "dotemacs:" custom-id)
                (concat sacha-dotemacs-url ":-:text=" (url-hexify-string title)))))
    (if combined
        (org-link-make-string
         url
         title)
      (cons url title))))

(eval-and-compile
  (require 'org-core nil t)
  (require 'org-macs nil t)
  (require 'org-src nil t))
(declare-function 'org-babel-tangle--compute-targets "ob-tangle")
(defun sacha-org-collect-links-for-tangled-files ()
  "Return a list of ((filename (link link link link)) ...)."
  (let* ((file (buffer-file-name))
         results)
    (org-babel-map-src-blocks (buffer-file-name)
      (let* ((info (org-babel-get-src-block-info))
             (link (sacha-dotemacs-link-for-section-at-point)))
        (mapc
         (lambda (target)
           (let ((list (assoc target results #'string=)))
             (if list
                 (cl-pushnew link (cdr list) :test 'equal)
               (push (list target link) results))))
         (org-babel-tangle--compute-targets file info))))
    ;; Put it back in source order
    (nreverse
     (mapcar (lambda (o)
               (cons (car o)
                     (nreverse (cdr o))))
             results))))
(defvar sacha-emacs-config-module-links nil "Cache for links from tangled files.")

;;;###autoload
(defun sacha-emacs-config-update-module-info ()
  "Update the list of links."
  (interactive)
  (setq sacha-emacs-config-module-links
        (seq-filter
         (lambda (o)
           (string-match "sacha-" (car o)))
         (sacha-org-collect-links-for-tangled-files)))
  (setq sacha-emacs-config-modules-info
        (mapcar (lambda (group)
                  `(,(file-name-base (car group))
                    (commentary
                     .
                     ,(replace-regexp-in-string
                       "^"
                       ";; "
                       (concat
                        "Related Emacs config sections:\n\n"
                        (org-export-string-as
                         (mapconcat
                          (lambda (link)
                            (concat "- " (cdr link) "\\\\\n  " (org-link-make-string (car link)) "\n"))
                          (cdr group)
                          "\n")
                         'ascii
                         t))))))
                sacha-emacs-config-module-links)))

;;;###autoload
(defun sacha-emacs-config-prepare-to-tangle ()
  "Update module info if tangling my config."
  (when (string-match "Sacha.org" (buffer-file-name))
    (sacha-emacs-config-update-module-info)))

Let's set up the functions for tangling the boilerplate.

(defvar sacha-emacs-config-modules-dir "~/sync/emacs/lisp/")
(defvar sacha-emacs-config-modules-info nil "Alist of module info.")
(defvar sacha-emacs-config-url "https://sachachua.com/dotemacs")

;;;###autoload
(defun sacha-org-babel-post-tangle-insert-boilerplate-for-sacha-lisp ()
  (when (file-in-directory-p (buffer-file-name) sacha-emacs-config-modules-dir)
    (goto-char (point-min))
    (let ((base (file-name-base (buffer-file-name))))
      (insert (format ";;; %s.el --- %s -*- lexical-binding: t -*-

;; Author: %s <%s>
;; URL: %s

;;; License:
;;
;; This file is not part of GNU Emacs.
;;
;; This is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:
;;
%s
;;; Code:

\n\n"
                      base
                      (or
                       (assoc-default 'description
                                      (assoc-default base sacha-emacs-config-modules-info #'string=))
                       "")
                      user-full-name
                      user-mail-address
                      sacha-emacs-config-url
                      (or
                       (assoc-default 'commentary
                                      (assoc-default base sacha-emacs-config-modules-info #'string=))
                       "")))
      (goto-char (point-max))
      (insert (format "\n(provide '%s)\n;;; %s.el ends here\n"
                      base
                      base))
      (save-buffer))))
(setq sacha-emacs-config-url "https://sachachua.com/dotemacs")
(with-eval-after-load 'org
  (add-hook 'org-babel-pre-tangle-hook #'sacha-emacs-config-prepare-to-tangle)
  (add-hook 'org-babel-post-tangle-hook #'sacha-org-babel-post-tangle-insert-boilerplate-for-sacha-lisp))

You can see the results at .emacs.d/lisp. For example, the function definitions in this post are at lisp/sacha-emacs.el.

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

You can view 2 comments or e-mail me at sacha@sachachua.com.

-1:-- Org Mode: Tangle Emacs config snippets to different files and add boilerplate (Post Sacha Chua)--L0--C0--2026-04-11T14:13:19.000Z

Listful Andrew: Phones-to-Words Challenge IV: Clojure as an alternative to Java

There's an old programming challenge where the digits in a list of phone numbers are converted to letters according to rules and a given dictionary file. The results of the original challenge suggested that Lisp would be a potentially superior alternative to Java, since Lisper participants were able to produce solutions in, on average, fewer lines of code and less time than Java programmers. Some years ago I tackled it in Emacs Lisp and Bash. I've now done it in Clojure.
-1:-- Phones-to-Words Challenge IV: Clojure as an alternative to Java (Post Listful Andrew)--L0--C0--2026-04-10T11:24:00.000Z

Erik L. Arneson: Emacs as the Freelancer's Command Center

Freelancing for small businesses and organizations leads to a position where you are juggling a number of projects for multiple clients. You need to keep track of a number of tasks ranging from software development to sending emails to project management. This is a lot easier when you have a system that can do a bunch of the work for you, which is why I use Emacs as my freelancer command center.

I would like to share some of the tools and workflows I use in Emacs to help me keep on top of multiple clients’ needs and expectations.

Organization with org-mode

It should be no surprise that at the center of my Emacs command center is org-mode. I have already written about it a lot. Every org-mode user seems to have their own way of keeping track of things, so please don’t take my organizational scheme as some kind of gospel. A couple of years ago, I wrote about how I handle to-do lists in org-mode, and I am still using that method for to-do keywords. However, file structure is also important. I have a number of core files.

Freelance.org

This top-level file contains all of my ongoing business tasks, such as tracking potential new clients, recurring tasks like website maintenance and checking my MainWP dashboard. I also have recurring tasks for invoicing, tracking expenses, and other important business things.

This file is also where I have my primary time tracking and reporting. Org-mode already supports this pretty nicely, I just use the built-in clocktable feature.

Clients/*.org

Clients that have large projects or ongoing business get their own file. This makes organization a lot easier. All tasks associated with a client and their various projects end up in these individual files. The important part is making sure that these files are included in the time-tracking clock table and your org-mode agenda, so you can see what is going on every week.

References and Linking

I have C-c l bound to org-store-link and use it all the time to link to various files, directories, URLs, and even emails. I can then use those links in my client notes, various tasks in my to-do list, and so on. This helps me keep my agenda organized even when my filesystem and browser bookmarks are a bit of a mess.

Email with mu4e

I have been reading and managing my email in Emacs for over 25 years. There have been a few breaks here and there where I have tried out other software or even web mail clients, but it has always been a headache. I return to Emacs! Long ago, I used VM (which seems to have taken on new life!), but currently I use mu4e.

This gives me a ton of power and flexibility when dealing with email. I have custom functions to help me compose and organize my email, and I can use org-store-link to keep track of individual emails from clients as they relate to agenda items. I even have a function to convert emails that I have written in Markdown into HTML email, and one that searches for questions in a client email to make sure I haven’t missed anything.

The ability to write custom code to both process and create email is extremely powerful and a great time saver.

Writing Code

I don’t know what else to say about this, I use Emacs for doing all of my software development. I make sure to use Eglot whenever there is a language server available, and I try to leverage all the fancy features offered by Emacs whenever possible. The vast majority of projects for clients are PHP (thanks WordPress), Go, JavaScript, and TypeScript.

Writing Words

Previously, I have shared quite a bit about writing in Emacs. I like to start everything in org-mode, but I also write quite a bit in Markdown. Emacs has become a powerful tool for writing. I use the Harper language server along with Eglot to check grammar and spelling.

Track All Changes with Magit

Version control is essential, a lesson I have learned over 30+ years of software development. While Git is not part of Emacs, the software I use to interface with Git is. Magit is a Git user interface that runs entirely in Emacs. I use it to track my writing, my source code, and even all of my org-mode files. Using version control is so essential that I have a weekly repeating agenda task reminding me to check all of my everyday files to make sure I have checked-in my changes for the week.

Thinking Music with EMMS

I like to have some soothing background music when I am programming, writing, or otherwise working on my computer. However, if that background music has lyrics, it can be really distracting. It is easy to make a playlist for various suitable SomaFM channels to load into EMMS (the Emacs Multimedia System) using the command M-x emms-play-playlist.

Try saving the following into playlist.el somewhere, and using it the next time you are writing:

 ;;; This is an EMMS playlist file Play it with M-x emms-play-playlist
 ((*track* (type . url) (name . "https://somafm.com/synphaera.pls"))
  (*track* (type . url) (name . "https://somafm.com/gsclassic.pls"))
  (*track* (type . url) (name . "https://somafm.com/sonicuniverse.pls"))
  (*track* (type . url) (name . "https://somafm.com/groovesalad.pls")))

And make sure to check out SomaFM’s selection to find some good background music that suits your tastes!

And the tools I have missed

There are undoubtedly Emacs tools that I have missed in this brief overview. I have been wracking my brain as I write, trying to see what I have forgotten or overlooked. Frankly, Emacs has become such a central part of the organization for my freelancing that there are probably many tools, packages, and processes that I use every day without thinking about it too much.

Emacs makes it possible for me to freelance for multiple clients and small businesses without losing my mind with organization and task management. The tools it provides allow me to stay on top of multiple projects, handle client relationships, and keep track of years worth of tasks, communications, and projects. Without it, I’d be sunk!

What Emacs tools are you using to manage your freelance business? I am always looking for ways to improve or streamline my process.

The featured image for this post comes from Agostino Ramelli’s Le diverse et artificiose machine (1588). Read more about it on the Public Domain Review.

-1:-- Emacs as the Freelancer's Command Center (Post Erik L. Arneson)--L0--C0--2026-04-10T00:00:00.000Z

Protesilaos Stavrou: Emacs modus-themes live stream today @ 14:00 Europe/Athens

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

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

At 14:00 Europe/Athens I will hold a live stream about Emacs. Specifically, I will work on my modus-themes package.

The idea is to write more tests and refine the relevant functions along the way.

I am announcing this -45 minutes before I go live. I will keep the chat open in case there are any questions.

-1:-- Emacs modus-themes live stream today @ 14:00 Europe/Athens (Post Protesilaos Stavrou)--L0--C0--2026-04-10T00:00:00.000Z

James Dyer: Wiring Flymake Diagnostics into a Follow Mode

Flymake has been quietly sitting in my config for years doing exactly what it says on the tin, squiggly lines under things that are wrong, and I mostly left it alone. But recently I noticed I was doing the same little dance over and over: spot a warning, squint at the modeline counter, run `M-x flymake-show-buffer-diagnostics`, scroll through the list to find the thing I was actually looking at, then flip back. Two windows, zero connection between them.

So I wired it up properly, and while I was in there I gave it a set of keybindings that feel right to my muscle memory.

The obvious bindings for stepping through errors are `M-n` and `M-p`, and most people using flymake bind exactly those. The problem is that in my config `M-n` and `M-p` are already taken, they step through simply-annotate annotations (which is itself a very handy thing and I am not giving it up!). So I shifted a key up and went with the shifted variants: `M-N` for next, `M-P` for previous, and `M-M` to toggle the diagnostics buffer.

 (setq flymake-show-diagnostics-at-end-of-line nil)
(with-eval-after-load 'flymake
(define-key flymake-mode-map (kbd "M-N") #'flymake-goto-next-error)
(define-key flymake-mode-map (kbd "M-P") #'flymake-goto-prev-error))

With M-M I wanted it to be a bit smarter than just “open the buffer”. If it is already visible I want it gone, if it is not I want it up. The standard toggle pattern:

 (defun my/flymake--diag-buffer ()
"Return the visible flymake diagnostics buffer, or nil."
(seq-some (lambda (b)
(and (with-current-buffer b
(derived-mode-p 'flymake-diagnostics-buffer-mode))
(get-buffer-window b)
b))
(buffer-list)))
(defun my/flymake-toggle-diagnostics ()
"Toggle the flymake diagnostics buffer."
(interactive)
(let ((buf (my/flymake--diag-buffer)))
(if buf
(quit-window nil (get-buffer-window buf))
(flymake-show-buffer-diagnostics)
(my/flymake-sync-diagnostics))))

Now the interesting bit. What I really wanted was a follow mode, something like how the compilation buffer tracks position or how Occur highlights the current hit. When my point lands on an error in the source buffer, the corresponding row in the diagnostics buffer should light up. That way the diagnostics window becomes a live index of where I am rather than a static dump and think in general this is how a lot of other IDEs work.

I tried the lazy route first, turning on hl-line-mode in the diagnostics buffer and calling hl-line-highlight from a post-command-hook in the source buffer. The line lit up once and then refused to move. Nothing I did would shift it. This is because hl-line-highlight is really only designed to be driven from the window whose line is being highlighted, and I was firing it from afar.

Ok, so why not just manage my own overlay:

 (defvar my/flymake--sync-overlay nil
"Overlay used to highlight the current entry in the diagnostics buffer.")
(defun my/flymake-sync-diagnostics ()
"Highlight the diagnostics buffer entry matching the error at point."
(when-let* ((buf (my/flymake--diag-buffer))
(win (get-buffer-window buf))
(diag (or (car (flymake-diagnostics (point)))
(car (flymake-diagnostics (line-beginning-position)
(line-end-position))))))
(with-current-buffer buf
(save-excursion
(goto-char (point-min))
(let ((found nil))
(while (and (not found) (not (eobp)))
(let ((id (tabulated-list-get-id)))
(if (and (listp id) (eq (plist-get id :diagnostic) diag))
(setq found (point))
(forward-line 1))))
(when found
(unless (overlayp my/flymake--sync-overlay)
(setq my/flymake--sync-overlay (make-overlay 1 1))
(overlay-put my/flymake--sync-overlay 'face 'highlight)
(overlay-put my/flymake--sync-overlay 'priority 100))
(move-overlay my/flymake--sync-overlay
found
(min (point-max) (1+ (line-end-position)))
buf)
(set-window-point win found)))))))

My first pass at the walk through the tabulated list did not work. I was comparing (tabulated-list-get-id) directly against the diagnostic returned by flymake-diagnostics using eq, and it was always false, which meant found stayed nil forever and the overlay never moved. A dive into flymake.el revealed why. Each row in the diagnostics buffer stores its ID as a plist, not as the diagnostic itself:

 (list :diagnostic diag
:line line
:severity ...)

So I need to pluck out :diagnostic before comparing. Obvious in hindsight, as these things always are. With plist-get in place the comparison lines up and the overlay moves exactly where I want it, tracking every navigation command.

The fallback lookup using line-beginning-position and line-end-position is there because flymake-diagnostics (point) only returns something if point is strictly inside the diagnostic span. When I land between errors or on the same line as an error but a few columns off, I still want the diagnostics buffer to track, so I widen the search to the whole line.

Finally, wrap the hook in a minor mode so I can toggle it per buffer and enable it automatically whenever flymake comes up:

 (define-minor-mode my/flymake-follow-mode
"Sync the diagnostics buffer to the error at point."
:lighter nil
(if my/flymake-follow-mode
(add-hook 'post-command-hook #'my/flymake-sync-diagnostics nil t)
(remove-hook 'post-command-hook #'my/flymake-sync-diagnostics t)))
(add-hook 'flymake-mode-hook #'my/flymake-follow-mode)
(define-key flymake-mode-map (kbd "M-M") #'my/flymake-toggle-diagnostics)

The end result is nice. M-M pops the diagnostics buffer, M-N and M-P walk through the errors, and as I navigate the source the matching row in the diagnostics buffer highlights in step with me. If I close the buffer with another M-M everything goes quiet, and I can still step through with M-N/M-P on their own.

Three little keybindings and twenty lines of elisp, but they turn flymake from a static reporter into something that actually feels connected to where I am in the buffer.

-1:-- Wiring Flymake Diagnostics into a Follow Mode (Post James Dyer)--L0--C0--2026-04-09T05:13:00.000Z

Charles Choi: Computing Days Until with Emacs

A recent Mastodon post showing the days until the next U.S. election got me to wonder, “how can I compute that in Emacs?” Turns out, this is trivial with the Org mode function org-time-stamp-to-now doing the timestamp computation for you.

We can wrap org-time-stamp-to-now in an internal function cc/--days-until that generates a formatted string of the days until a target date.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(defun cc/--days-until (target &optional template)
  "Formatted string of days until TARGET.

- TARGET: date string that conforms to `parse-time-string'.
- TEMPLATE : format string that includes ‘%d’ specifier.

If TEMPLATE is nil, then a predefined format string will be
used."
  (let* ((template (if template
                       template
                     (concat "%d days until " target)))
         (days (org-time-stamp-to-now target))
         (msg (format template days)))
    msg))

From there we can then start defining commands that use cc/--days-until. The command cc/days-until shown below will prompt you with a date picker to enter a date. Note that you can enter a date value (e.g. “Dec 25, 2026”) in the mini-buffer prompt for org-read-date.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(defun cc/days-until (arg)
  "Prompt user for date and show days until in the mini-buffer.

Use `org-read-date' to compute days until to display in the mini-buffer.

If prefix ARG is non-nil, then the computed result is stored in the
 `kill-ring'."
  (interactive "P")
  (let* ((target (org-read-date))
         (msg (cc/--days-until target)))
    (if arg
        (kill-new msg))
    (message msg)))

Going back to the original motivator for this post, here’s an implementation of days until the next two major U.S. election dates with the command cc/days-until-voting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(defun cc/days-until-voting (arg)
  "Days until U.S. elections in 2026 and 2028.

If prefix ARG is non-nil, then the computed result is stored in the
 `kill-ring'."
  (interactive "P")
  (let* ((midterms (cc/--days-until "2026-11-03" "%d days until 2026 midterms"))
         (election (cc/--days-until "2028-11-07" "%d days until 2028 presidential election"))
         (msg (format "%s, %s" midterms election)))
    (if arg
        (kill-new msg))
    (message msg)))

The result of M-x cc/days-until-voting as of 8 April 2026 is:

209 days until 2026 midterms, 944 days until 2028 presidential election

It’s so human to want to know how long it’s going to take. Feel free to build your own countdown clocks using the code above. May your journey to whatever you plan be a happy one!

-1:-- Computing Days Until with Emacs (Post Charles Choi)--L0--C0--2026-04-08T23:00:00.000Z

Dave Pearson: quiz.el v1.7

I wondered yesterday:

...those question headers are displaying differently, with the background colour no longer spanning the width of the window. I'd like to understand why.

Turns out it was pretty straightforward:

diff --git a/quiz.el b/quiz.el
index 2dbe45d..c1ba255 100644
--- a/quiz.el
+++ b/quiz.el
@@ -40,7 +40,8 @@
 (defface quiz-question-number-face
   '((t :height 1.3
        :background "black"
-       :foreground "white"))
+       :foreground "white"
+       :extend t))
   "Face for the question number."
   :group 'quiz)

and so v1.7 has happened.

Quiz with reinstated header look

It looks like, perhaps, at some point in the past, :extend was t by default, but it no longer is? Either way, explicitly setting it to t has done the trick.

-1:-- quiz.el v1.7 (Post Dave Pearson)--L0--C0--2026-04-08T15:54:38.000Z

Irreal: Tolerance For Repetition

Floofcode over that the Emacs subreddit asks a question that resonates with me. He notes that he often has a repetitive task and wonders whether it would be worthwhile writing some Elisp to automate it. Usually, he has to repeat the task several times before he gets fed up and fixes it for good. He wonders how other people deal with this. Do they have to repeat the task a certain number of times before automating it or is the criterion more subjective.

I can relate. This happens to me all the time. I keep doing the same task over and over until one day I realize that I’m being stupid and spend a few minutes dashing off a bit of Elisp that solves the problem once and for all. Every time, I tell myself, “Well, I won’t that mistake again. Next time I’m going to get this type of task automated right away.” Of course, the next time the same thing happens.

As to floofcode’s question, I would guess that it depends on the person. For me, it’s a subjective matter. The amount of time I’ll spend repeating the same boring task over and over varies but it always ends in a fit of anger when I ask myself why I’m still doing things manually. The thing is, when I’m repeatedly doing the task manually, I’m not wondering whether I should automate it. That happens at the end when I realize I’ve been stupid.

I guess the answer is something of the sort that after you’ve repeated the task twice, just automate it. Sure sometimes you’ll lose and waste time but in my experience it will most often be a win. I wish I could learn this.

-1:-- Tolerance For Repetition (Post Irreal)--L0--C0--2026-04-08T14:33:12.000Z

Dave Pearson: fasta.el v1.1

Today's Emacs Lisp package tidy-up is of a package I first wrote a couple of employers ago. While working on code I often found myself viewing FASTA files in an Emacs buffer and so I thought it would be fun to use this as a reason to knock up a simple mode for highlighting them.

fasta.el was the result.

An example FASTA file

While I doubt it was or is of much use to others, it helped me better understand simple font-locking in Emacs Lisp, and also made some buffers look a little less boring when I was messing with test data.

As for this update: it's the usual stuff of cleaning up deprecated uses of setf, mostly.

If bioinformatics-related Emacs Lisp code written by a non-bioinformatician is your thing, you might also find 2bit.el of interest too. Much like fasta.el it too probably doesn't have a practical use, but it sure was fun to write and taught me a few things along the way; it also sort of goes hand-in-hand with fasta.el too.

-1:-- fasta.el v1.1 (Post Dave Pearson)--L0--C0--2026-04-08T10:25:43.000Z

Charles Choi: Calming Mouse Interaction in Dired

Conventional file managers have conditioned me to expect that a single left-button mouse click (<mouse-1>) will select a file (or directory) and double-click will open it. This is not the default behavior of Dired, where a single click is an open action. I find this far too twitchy for my taste.

This post shows how to make Dired mouse interaction align with a conventional file manager where:

  • Single-click on a file or directory will move the point to it, making it the implicit target for any subsequent Dired command.

  • Left double-click on file or directory will open it.

  • Selecting multiple files is emulated using Dired marking, in this case using the binding M-<mouse-1> to toggle marking a file.

The first two points above can be addressed with the global variable mouse-1-click-follows-link. Dired uses this variable to control its mouse behavior, but we don’t want to change it everywhere, just for Dired buffers. This can be implemented by setting mouse-1-click-follows-link locally as a hook to dired-mode-hook:

1
2
3
4
(add-hook
 'dired-mode-hook
 (lambda ()
   (setq-local mouse-1-click-follows-link 'double)))

To address multiple file selection, we can define a function cc/dired-mouse-toggle-mark and bind it to M-<mouse-1>.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(defun cc/dired-mouse-toggle-mark ()
  "Toggle mark of a Dired item via mouse."
  (interactive)
  (unless (use-region-p)
    (mouse-set-point last-input-event)
    (if (char-equal (char-after (line-beginning-position)) dired-marker-char)
        (call-interactively #'dired-unmark)
      (call-interactively #'dired-mark))))

(keymap-set dired-mode-map "M-<mouse-1>" #'cc/dired-mouse-toggle-mark)

Coupled with Anju support for a Dired specific context menu and many basic file manager operations can be done in Dired via mouse with minimal fuss.

-1:-- Calming Mouse Interaction in Dired (Post Charles Choi)--L0--C0--2026-04-07T20:25:00.000Z

Irreal: Karthik On Repeat Mode

The other day, I wrote about repeat mode. My take was that it was a way repeating certain commands without having to retype their, possibly, complex prefixes. All of that is true but as Karthik informed me in a comment, there is much, much more to repeat mode than simply repeating commands.

It is a way, he says, of grouping a set of related commands together into a sort of mode. Thus, there is more to Ctrl+x } than simply repeating the enlarge window command. Once you type Ctrl+x you can type any of {, }, ^, v to resize the window in any direction. The Ctrl+x enables a keymap with those four single keys to resize the current window, defining, in effect, a “resize window mode”.

Four years ago, Karthik wrote a long post that explains all this and, at least on an intuitive level, how it works. My first thought was to add an update to my post that pointed to Karthik’s and I did that but then I thought that his post was so good that I should devote a new post to it so that anyone who missed it the first time would see it.

Repeat mode really is an excellent facility—Karthik says it’s a cornerstone of his Emacs usage—and every Emacser should be familiar with it. If nothing else, it’s worth enabling repeat mode so that you can use the built in repeat maps. You can see what they are by running the command describe-repeat-maps.

-1:-- Karthik On Repeat Mode (Post Irreal)--L0--C0--2026-04-07T15:59:32.000Z

Dave Pearson: quiz.el v1.6

A quick little refresh of one of my old packages, this time quiz.el. This is a nice little distraction when you're working in Emacs, letting you spin up a quick trivia quiz in a buffer.

Quiz in action

It's backed by the Open Trivia Database, so there's a good few subjects, questions, and levels of difficulty to play with.

The only changes I've made to it in this release are the usual clean-ups of the deprecated uses of setf, plus I've added q as a binding to the quiz window to quickly quit the quiz.

I might have to come back and revisit it soon, as it looks like the default face choices could probably do with a rethink, and I can see at the moment that the attempt at a window-wide "header" for each question isn't working any longer. For comparison, here's how the package looked when running back when I first wrote it back in 2017:

How it originally looked

Leaving aside the fact that I was still running a very light Emacs then, those question headers are displaying differently, with the background colour no longer spanning the width of the window. I'd like to understand why.

-1:-- quiz.el v1.6 (Post Dave Pearson)--L0--C0--2026-04-07T08:18:23.000Z

Emacs Redux: Stealing from the Best Emacs Configs

Good artists borrow, great artists steal.

– Pablo Picasso

After spending the past couple of weeks updating Prelude and my personal Emacs config, I figured it wouldn’t hurt to see what the competition has been up to. I hadn’t done a proper survey of other people’s configs in years, and the Emacs landscape has changed quite a bit since the last time I looked.

So I went through Doom Emacs, Purcell’s emacs.d, Centaur Emacs, Prot’s dotfiles, and a handful of others. Here are some of the most interesting things I found – settings and tricks that I either didn’t know about or had forgotten about entirely.

Performance Tweaks

Disable Bidirectional Text Scanning (Doom Emacs)

If you don’t edit right-to-left languages (Arabic, Hebrew, etc.), Emacs is doing a bunch of work on every redisplay cycle for nothing. These settings tell Emacs to assume left-to-right text everywhere and skip the bidirectional parenthesis algorithm:

(setq-default bidi-display-reordering 'left-to-right
              bidi-paragraph-direction 'left-to-right)
(setq bidi-inhibit-bpa t)

The difference is hard to measure in small buffers, but in large files (think multi-thousand-line JSON or log files) it adds up. Doom enables this unconditionally and I’ve never seen anyone complain about it.

Skip Fontification During Input (Doom Emacs)

Emacs normally fontifies (syntax-highlights) text even while you’re actively typing. This can cause micro-stutters, especially in tree-sitter modes or large buffers. One setting fixes it:

(setq redisplay-skip-fontification-on-input t)

Emacs will defer fontification until you stop typing. In practice you never notice the delay – the highlighting catches up instantly – but scrolling and typing may feel smoother.

Increase Process Output Buffer for LSP (Doom, Purcell, Centaur)

The default read-process-output-max is 64KB, which is still quite conservative. Modern LSP servers like rust-analyzer or clangd routinely send multi-megabyte responses. Bumping this reduces the number of read calls Emacs has to make:

(setq read-process-output-max (* 4 1024 1024)) ; 4MB

If you use eglot (or lsp-mode), this is basically free performance. Three of the most popular configs out there all set it – that should tell you something.

Note: I’m really surprised I didn’t discover this one sooner. Probably that’s because I rarely work on big projects these days.

Don’t Render Cursors in Non-Focused Windows (Doom Emacs)

If you have several windows visible, Emacs draws a cursor in each of them – even the ones you’re not working in. It also highlights selections in non-focused windows. Two settings to stop that:

(setq-default cursor-in-non-selected-windows nil)
(setq highlight-nonselected-windows nil)

This is mostly a visual preference (I don’t mind the phantom cursors but I know some people find them distracting), but it also reduces rendering work.

All four of these performance settings are safe to add unconditionally – they have no downsides for the vast majority of users.

Kill Ring (Emacs’s Clipboard History) and Clipboard

Save the Clipboard Before Killing (Purcell, Prot, Centaur)

Here’s a scenario: you copy a URL from your browser, switch to Emacs, kill a line with C-k, and then try to yank the URL you copied earlier with C-y. Gone. The kill replaced it on the clipboard.

This setting makes Emacs save the existing clipboard content into the kill ring before overwriting it:

(setq save-interprogram-paste-before-kill t)

Now C-y gets the kill, and M-y gets you back to the URL. Such a small thing, but it eliminates a genuinely annoying problem.

No Duplicates in the Kill Ring (Doom, Prot)

Kill the same line three times and you get three identical entries in the kill ring, wasting slots. This deduplicates them:

(setq kill-do-not-save-duplicates t)

Persist the Kill Ring Across Sessions (Doom, Prot)

Most configs that use savehist-mode only persist search rings. But savehist can save any variable – including the kill ring. Add it and you get clipboard history that survives restarts:

(setq savehist-additional-variables
      '(search-ring regexp-search-ring kill-ring))

One thing to watch out for: the kill ring can accumulate text properties (fonts, overlays, etc.) that bloat the savehist file. Doom strips them before saving:

(add-hook 'savehist-save-hook
          (lambda ()
            (setq kill-ring
                  (mapcar #'substring-no-properties
                          (cl-remove-if-not #'stringp kill-ring)))))

Probably overkill for most people, but it’s good to be aware of if your savehist file starts growing suspiciously large.

Editing

Auto-Chmod Scripts on Save (Multiple Configs)

If you create a file that starts with #! (a shebang line), it should be executable. But you always forget to chmod +x it, run the script, get “Permission denied”, curse, go back, chmod, try again. This hook does it automatically:

(add-hook 'after-save-hook
          #'executable-make-buffer-file-executable-if-script-p)

Save a file with a shebang, and Emacs chmod +xes it for you. One of those things that should arguably be a default.

Note: Okay, I have to admit I’ve always known this one, but seeing it in so many configs made me want to include it here.

Sane Syntax in re-builder (Multiple Configs)

re-builder (M-x re-builder) is an interactive tool for developing regexps – you type a pattern and see matches highlighted live in the target buffer. The problem is the default syntax: read. In read syntax, you have to double-escape everything, so a word boundary is \\< and a group is \\(...\\). It’s the regexp equivalent of trying to type with oven mitts on.

Switch to string syntax and things look like normal Emacs regexps:

(setq reb-re-syntax 'string)

Now \< is \< and \(foo\) is \(foo\). Much less painful.

See also: If you want live feedback on the regexp structure as you type it (color-coded groups, character classes, etc.), check out minibuffer-regexp-mode – a new built-in mode in Emacs 30.

Prevent ffap from Pinging Hostnames (Centaur Emacs)

Ever had Emacs freeze for a few seconds when you ran find-file-at-point (or a command that uses it internally)? If the text under point looks like a hostname – say, something.com in a comment – ffap tries to ping it to check if it’s reachable. On a slow or firewalled network, that’s a multi-second hang.

(setq ffap-machine-p-known 'reject)

This tells ffap to never try network lookups. If you actually want to open a remote file, you can type the path explicitly.

Windows

Proportional Window Resizing (Purcell, Prot)

When you split a window with C-x 2 or C-x 3, Emacs halves the current window. If you already have a multi-window layout, this can produce one awkwardly tiny window while others stay large. With this setting, all windows in the frame resize proportionally:

(setq window-combination-resize t)

The difference is subtle but makes multi-window layouts feel more balanced without manual resizing.

Reversible C-x 1 (Purcell)

C-x 1 (delete-other-windows) is the nuclear option – it nukes your entire window layout to focus on one buffer. Then you spend the next minute recreating the layout you just destroyed.

With winner-mode and a small wrapper, you can make C-x 1 toggle: press it once to go single-window, press it again to restore the previous layout:

(winner-mode +1)

(defun toggle-delete-other-windows ()
  "Delete other windows in frame if any, or restore previous window config."
  (interactive)
  (if (and winner-mode
           (equal (selected-window) (next-window)))
      (winner-undo)
    (delete-other-windows)))

(global-set-key (kbd "C-x 1") #'toggle-delete-other-windows)

Just drop this into your config as-is – it’s self-contained. This is one of those tricks where once you have it, you can’t imagine going back.

Misc

Faster Mark Popping (Purcell, Centaur, Prot)

The mark ring is one of Emacs’s most underused navigation features. Every time you jump somewhere – isearch, M-<, M->, goto-line, imenu, and many more – Emacs pushes your old position onto the mark ring. C-u C-SPC pops it, jumping you back.

The annoyance: you need C-u C-SPC every single time. With this setting, after the first C-u C-SPC you can keep pressing just C-SPC to continue popping:

(setq set-mark-command-repeat-pop t)

This pairs beautifully with repeat-mode if you have it enabled (and you should – see my earlier post on repeat-mode).

Recenter After save-place Restores Position (Doom Emacs)

save-place-mode is great – it remembers where you were in each file and jumps back there when you reopen it. The problem is that it can leave your cursor on the last visible line of the window, which is disorienting. This advice recenters the view after the jump:

(advice-add 'save-place-find-file-hook :after
            (lambda (&rest _)
              (when buffer-file-name (ignore-errors (recenter)))))

Small thing, but it makes reopening files feel much more natural.

Auto-Select Help Windows (Prot)

When you press C-h f or C-h v, Emacs opens the help buffer but leaves your cursor in the original window. You almost always want to read the help right away, so you end up pressing C-x o every single time. This fixes it:

(setq help-window-select t)

Bonus: Many of the configs I surveyed also use built-in lazy isearch counting (showing “match N of M” in the minibuffer) instead of third-party packages like anzu. I recently wrote about that in a dedicated post.


The funny thing about all of this is how much overlap there is between configs. Half of these tricks appear in three or four of the configs I surveyed. At this point I’m convinced there are about 200 essential Emacs settings floating around in the collective unconscious, and every serious config independently converges on roughly the same subset. Picasso was right – we all steal from each other, and the kill ring makes it embarrassingly easy. M-w and move on.

That’s all I have for you today! Keep hacking!

-1:-- Stealing from the Best Emacs Configs (Post Emacs Redux)--L0--C0--2026-04-07T06:00:00.000Z

Dave Pearson: expando.el v1.5

While I have been doing a lot of hacking on blogmore.el, I haven't forgotten my plan to revisit and refresh some of my older personal packages. This evening I've paid some attention to expando.el.

This started life a long time ago, as part of my grab-bag of handy functions that got carried around and copied from machine to machine, until I did a big tidy-up of everything back in 2017 and turned various things into packages that I managed via a self-hosted (well, GitHub pages hosted) package index.

It's a pretty simple but very useful bit of code that lets me quickly macroexpand a sexp at point and pretty print it into a display window. I've often found it indispensable when it came to writing my own macros.

expando in action

This release simply adds a lexical-binding header to the file, and also adds a q key binding to the resulting view window so that it can be quickly and easily closed.

Also, as with all my other personal packages, I've swapped away from using delpa to simply using :vc to pull it in.

(use-package expando
  :vc (:url "https://github.com/davep/expando.el" :rev :newest)
  :bind
  ("C-c e" . expando-macro))

Or perhaps I should say...

(progn
  (use-package-vc-install
   '(expando (:url "https://github.com/davep/expando.el") nil) nil)
  (defvar use-package--warning69
    #'(lambda (keyword err)
        (let
            ((msg
              (format "%s/%s: %s" 'expando keyword
                      (error-message-string err))))
          (display-warning 'use-package msg :error))))
  (condition-case err
      (progn
        (if (fboundp 'expando-macro) nil
          (autoload #'expando-macro "expando" nil t))
        (let*
            ((name "C-c e") (key [3 101])
             (kmap
              (or (if (and nil (symbolp nil)) (symbol-value nil) nil)
                  global-map))
             (kdesc
              (cons (if (stringp name) name (key-description name))
                    (if (symbolp nil) nil 'nil)))
             (binding (lookup-key kmap key)))
          (require 'bind-key)
          (let
              ((entry (assoc kdesc personal-keybindings))
               (details
                (list #'expando-macro (if (numberp binding) nil binding))))
            (if entry (setcdr entry details)
              (add-to-list 'personal-keybindings (cons kdesc details))))
          (define-key kmap key #'expando-macro)))
    ((debug error) (funcall use-package--warning69 :catch err))))
-1:-- expando.el v1.5 (Post Dave Pearson)--L0--C0--2026-04-06T19:43:29.000Z

Dave Pearson: blogmore.el v4.0

Despite having bumped it from 2.x to 3.x yesterday, I'm calling v4.0 on blogmore.el today. There's a good reason for this though. While tinkering with some of the configuration yesterday, and also answering a configuration question last night, I realised that it made sense to make some of the internals into public utility functions.

Now, sure, Emacs Lisp doesn't really have internals in the private function sense, but I've always liked the approach that a package-- prefix communicates "internal, might go away" vs package- which tells me "this is a stable part of the API of this package". With this in mind I've always tried to write my code using this convention. I did this with blogmore.el too and a lot of the code had the blogmore-- prefix.

There's plenty of code in there that someone might want to make use of, if they wanted to add their own commands, or do fun things with the configuration. So with this in mind I've "promoted" a bunch of code to being "public" and, in that case, I feel this deserves another major version bump1.

Things that are now part of the "public" interface include:

  • blogmore-clean-time-string
  • blogmore-get-frontmatter
  • blogmore-remove-frontmatter
  • blogmore-set-frontmatter
  • blogmore-slug
  • blogmore-toggle-frontmatter
  • blogmore-with-post

Each one is documented via its docstring (just a quick Ctrl+h f function-name RET away) and hopefully is pretty self-explanatory.

blogmore-with-post is especially handy as it provides a quick and easy way of pulling information from a post file. So something like this:

(blogmore-with-post "~/write/davep.github.com/content/posts/2026/04/2026-04-05-blogmore-el-v3-1.md"
  (list
   (blogmore-get-frontmatter "title")
   (blogmore-get-frontmatter "date")))

resulting in:

("blogmore.el v3.1" "2026-04-05 20:04:44+0100")

Meaning that this snippet from yesterday's post:

(with-temp-buffer
  (insert-file-contents-literally file)
  (parse-iso8601-time-string
   (blogmore--clean-time-string (blogmore--get-frontmatter-property "date"))))

becomes:

(blogmore-with-post file
  (parse-iso8601-time-string
   (blogmore-clean-time-string (blogmore-get-frontmatter "date"))))

Not massively different, but it reads better and now all the calls are to the "public API" of the package.

Not all the changes are "promoted internals". I've also added a blogmore-remove-tag command (and also added it to the transient menu).

Removing a tag

I've also changed the way that blogmore-add-tag works so that, now, if it's called from the transient, it immediately goes back to the tag input prompt, allowing for another tag to be immediately selected (you can quit out of this with Ctrl+g). Removal of a tag works in a similar way, making things a lot quicker.

I've also added some extra tests too, which makes it even easier for me to make future changes with confidence. The more I work with it the more I appreciate that ERT is available.


  1. Ordinarily it shouldn't matter as the public interface isn't changing, but some of the "internal" functions had been mentioned as options for configuration. 

-1:-- blogmore.el v4.0 (Post Dave Pearson)--L0--C0--2026-04-06T16:04:16.000Z

Chris Maiorana: The Emacs Way: Deleting Files

I know it’s scary, but sometimes you have to delete files, and once they’re gone—they’re gone. I guess that was why GUI systems invented the trash folder. The trash is a safe place to store files you want to delete, just in case you made a mistake.

In the UNIXy and Emacs worlds, once you delete that file, it’s gone, so you better have your story straight.

UNIXy Way

In UNIXy world, you can delete files with the trusty rm (remove) command, as follows:

rm file.txt
rm -rf directory/

Of course, once you have run that command, the file will be totally obliterated with no trace.

While you should never do this, you should be aware of the dreaded nuclear solution to delete your whole filesystem from the root level:

rm -rf /*

Emacs Way

The Emacs way of deleting files will also obliterate files entirely, but with dired you get the added safety of marking files first with the d key. Once you have made your selections, you can use the x key to execute the deletion operation

You can also use capital D for immediate deletion.

Likewise, if you wanted to run deletions interactively, you can run the functions directly:

  • M-x delete-file
  • M-x delete-directory

So that’s how you can delete files using your command line, in UNIXy world, or your “dired” directory editor in Emacs.

The post The Emacs Way: Deleting Files appeared first on Chris Maiorana.

-1:-- The Emacs Way: Deleting Files (Post Chris Maiorana)--L0--C0--2026-04-06T16:00:30.000Z

Sacha Chua: YE12: Categorizing Emacs News, epwgraph, languages

View in the Internet Archive, watch or comment on YouTube, or email me.

Chapters:

  • 00:41:21 epwgraph
  • 00:54:56 learning languages

Thanks for your patience with the audio issues! At some point, I need to work out the contention between all the different processes that I want to be listening to the audio from my mic. =)

In this livestream, I categorize Emacs News for 2026-04-06, show epwgraph for managing Pipewire connections from Emacs, and share some of my language learning workflows.

View Org source for this post

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

-1:-- YE12: Categorizing Emacs News, epwgraph, languages (Post Sacha Chua)--L0--C0--2026-04-06T14:36:57.000Z

Irreal: Read Extended Command Predicate

Bozhidar Batsov has a post that mentions how M-x shows a lot of commands many of which make no sense in the current context. This has never bothered me because after inputting a few (fuzzy) letters, the display converges on the command I’m looking for. Others, are not so sanguine and find the long list annoying.

It turns out that there’s an easy fix for this: read-extended-command-predicate. As of Emacs 28, You can set this variable to one of 3 values—or nil​—to control the filtering of commands in the candidate list. These are:

command-completion-default-include-p
Exclude on those commands tagged for a different mode.
command-completion-using-modes-and-keymaps-p
Include only commands tagged for the current buffer and those commands that have a keybinding active in the current buffer.
command-completion-using-modes-p
Show only commands tagged for the current mode.

The first option is the most conservative and general and is what Batsov recommends for everyday use.

Batsov also explains how functions can declare what mode or modes they’re appropriate for. That’s simply a matter of listing them in the interactive declaration. There are a lot more details in Batsov’s post so be sure to take a look. The read-extended-command-predicate mechanism is similar to the execute-extended-command-for-buffer mechanism that I wrote about previously.

As I said, the long list doesn’t bother me but if it annoys you, this may be the answer.

Update [2026-04-07 Tue 12:05]: prefix → predicate

-1:-- Read Extended Command Predicate (Post Irreal)--L0--C0--2026-04-06T14:22:46.000Z

Emacs Redux: The Many Faces of flet: cl-flet, cl-labels, and cl-letf

Way back in 2013 I wrote about the deprecation of flet and how noflet could fill the gap. Thirteen years later, it’s probably time for a proper overview of what replaced flet in cl-lib and when to use each option.

Emacs Lisp doesn’t have a built-in way to define local functions (the way let defines local variables), so cl-lib provides several macros for this. If you’ve ever been confused by cl-flet, cl-labels, and cl-letf – you’re not alone. The naming doesn’t make the distinctions obvious, and the documentation is a bit dry. Let’s try to fix that.

A Bit of History

The original flet (from the old cl package) let you temporarily override a function’s definition. It worked by swapping out the function’s symbol-function cell and restoring it when the body finished – essentially a dynamic let for functions:

;; Old-style flet (deprecated since Emacs 24.3)
(flet ((some-function () "overridden result"))
  ;; Everything here, including called functions, sees the override
  (some-function))

This was very handy for testing (stubbing impure functions), but it conflated two different things into one macro:

  1. Defining local helper functions (a lexical concept)
  2. Temporarily overriding a global function (a dynamic concept)

When cl was reorganized into cl-lib in Emacs 24.3, flet was split into separate macros for each use case. This also brought the lexical variants in line with Common Lisp semantics, where flet and labels are lexically scoped.

The Three Replacements

cl-flet: Local Functions (No Recursion)

cl-flet binds function names lexically within its body. The key thing to understand is that the binding is only visible in the body forms – not inside the function’s own definition, and not to any functions you call:

(cl-flet ((double (n) (* n 2)))
  (double 21)) ; => 42

Because it’s lexical, cl-flet cannot override functions seen by other code:

(defun my-helper () (+ 1 2))
(defun my-caller () (my-helper))

(cl-flet ((my-helper () 999))
  (my-caller))  ; => 3, NOT 999!

my-caller still sees the original my-helper. This is the fundamental difference from the old flet.

There’s also cl-flet*, which is to cl-flet what let* is to let – each binding can reference the ones before it.

Use cl-flet when you just need a simple local helper and don’t need recursion. Think of it as let for functions.

cl-labels: Local Functions (With Recursion)

cl-labels is like cl-flet, but the function is visible inside its own body and inside the bodies of sibling bindings. This makes recursion and mutual recursion possible:

(cl-labels ((factorial (n)
              (if (<= n 1) 1
                (* n (factorial (- n 1))))))
  (factorial 10)) ; => 3628800

This would blow up with cl-flet because factorial wouldn’t be defined inside its own body.

Mutual recursion works too:

(cl-labels ((my-even-p (n) (if (= n 0) t (my-odd-p (- n 1))))
            (my-odd-p (n) (if (= n 0) nil (my-even-p (- n 1)))))
  (list (my-even-p 4) (my-odd-p 3))) ; => (t t)

Use cl-labels when your local functions need to call themselves or each other.

Note: cl-labels requires lexical-binding to be t in the file (which it really should be for any modern Emacs Lisp code).

cl-letf: Temporary Global Override

This is the one that actually replaces the old flet’s dynamic behavior. cl-letf temporarily rebinds a generalized place (anything setf can handle) and restores it on exit:

(defun my-helper () (+ 1 2))
(defun my-caller () (my-helper))

(cl-letf (((symbol-function 'my-helper) (lambda () 999)))
  (my-caller))  ; => 999

Now my-caller does see the override, because cl-letf modifies the actual symbol-function cell of my-helper – just like the old flet did. The original definition is restored when the body exits, even on error.

The syntax is a bit verbose because cl-letf isn’t specific to functions – it’s a general-purpose temporary binding macro for any setf-able place. (symbol-function 'name) is a “generalized variable” that refers to the function stored in a symbol’s function cell – it’s just one of many places cl-letf can bind. For example:

;; Temporarily silence messages
(cl-letf (((symbol-function 'message) #'ignore))
  (do-something-noisy))

Use cl-letf when you need the old dynamic flet behavior – typically for testing (stubbing functions) or temporarily suppressing/redirecting behavior.

Quick Reference

  Scope Recursion Overrides global?
cl-flet Lexical No No
cl-labels Lexical Yes No
cl-letf Dynamic N/A Yes

In other words:

  • Default to cl-flet for local helpers. It’s the simplest and most predictable.
  • Reach for cl-labels when you need recursion or mutual recursion in local functions.
  • Use cl-letf only when you genuinely need dynamic override – mainly in tests. Modifying global function cells is a sharp tool and it’s not thread-safe, so keep it contained.

That’s all I have for you today. Keep hacking!

-1:-- The Many Faces of flet: cl-flet, cl-labels, and cl-letf (Post Emacs Redux)--L0--C0--2026-04-06T12:00:00.000Z

Emacs Redux: Live Regexp Feedback with minibuffer-regexp-mode

This is the third article in a small series inspired by my recent cleanup of Prelude and my personal Emacs configuration, following the ones on repeat-mode and read-extended-command-predicate. I’ve been going through the Emacs 28-30 changelogs for features I had ignored so far, and this one from Emacs 30 immediately caught my eye.

Writing Emacs regexps has always been a bit of a dark art. Between the double-escaped backslashes and the various group syntaxes (\(...\), \(?:...\), \(?N:...\)), it’s easy to lose track of what you’re actually matching. You type something into query-replace-regexp, press RET, and hope for the best.

Emacs 30 added minibuffer-regexp-mode, a minor mode that gives you live visual feedback as you compose a regexp in the minibuffer:

(minibuffer-regexp-mode 1)

When active, the mode highlights the structure of your regexp as you type it in the minibuffer. Capture groups, character classes, and other constructs get color-coded so you can see at a glance whether your grouping is right.

I find this particularly useful when building a regexp with multiple groups for query-replace-regexp, where you need to get the group numbering right for the replacement string (e.g., \1, \2). The visual feedback makes it obvious which group is which.

How Does This Compare to re-builder?

You might be wondering how this compares to re-builder (M-x re-builder). They’re complementary, really. re-builder shows matches in the buffer as you type a regexp in a dedicated editing window – great for developing complex patterns against actual text. minibuffer-regexp-mode, on the other hand, highlights the regexp itself in the minibuffer. It kicks in automatically whenever you’re prompted for a regexp (e.g., isearch-forward-regexp, query-replace-regexp, keep-lines, etc.).

One helps you see what your regexp matches; the other helps you see what your regexp says. I’d suggest using both.

That’s all I have for you today. Keep hacking!

-1:-- Live Regexp Feedback with minibuffer-regexp-mode (Post Emacs Redux)--L0--C0--2026-04-06T09:00:00.000Z

Protesilaos Stavrou: Emacs live stream for writing Denote tests and more on Monday 6 April @ 20:00 Europe/Athens

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

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

Tonight I will work on my denote package. There is a feature branch I implemented this morning and am now ready to continue refining the code. The immediate goals:

  • Update unit tests that are still calling deprecated functions.
  • Write new tests, starting with the denote-dired command and all its ancillary functions.
  • Review all the commands that filter the query buffers (which are produced by commands such as denote-grep, denote-backlinks, denote-query-contents-link).
  • Edit the manual accordingly.

I expect the stream to go on for 2-3 hours, but we will see.

I will keep the chat open in case there are any comments. I am happy to respond to them.

-1:-- Emacs live stream for writing Denote tests and more on Monday 6 April @ 20:00 Europe/Athens (Post Protesilaos Stavrou)--L0--C0--2026-04-06T00:00:00.000Z

Dave Pearson: blogmore.el v3.1

When I first started writing blogmore.el it was just going to be a handful of commands that let me spin up a new blog post, and insert the odd link here and there when needed. Initially it only handled a single blog, and everything it did was based around how I lay my personal blog out, and was also very much geared to how I'd made BlogMore work.

But then I wanted to use it to edit both my personal blog and my photoblog. So then I had to add support for configuring different ways of laying out posts for different blogs, etc.

Still, it was mostly written as a personal tool that worked for my stuff. I tried to make it so that it was easy enough to configure (and let's be fair: it's for Emacs and written in Emacs Lisp, it's kind of hard to not be very configurable if you're happy to get your hands dirty with some coding), but there were still some parts of it that weren't as easy to change as I'd have liked.

Also, when I'd originally added the multi-blog configuration, I'd chosen a format for the list of blogs that was an assoc-list of pure lists wrapped by a cl-defstruct to make for easier access. It worked well but was very positional in its nature.

So when the request came in to be able to have better control over the name of the file when starting a new post, which meant I was going to need to rearrange the structure again, it was time to try and do something about it.

Which is how I'm now on v3.1 (yes, there was a v3.0 but I quickly found something in that that needed fixing1). It's a major version bump because I've totally changed how the blogmore-blogs variable holds the data.

From now on I'm using EIEIO to create a class that holds all of the data for a given blog. This, I believe, makes the code easier to read and should also make it more resilient to the addition of any new properties. Also thanks to how such classes can work with the customize system the customize experience remains pretty much the same too.

Personally I don't use the customize UI, instead I declare everything via use-package. As of the time of writing my declaration for blogmore looks like this:

(use-package blogmore
  :ensure t
  :defer t
  :vc (:url "https://github.com/davep/blogmore.el" :rev :newest)
  :init
  (add-hook 'blogmore-new-post-hook #'end-it)
  (blogmore-work-on "blog.davep.org")
  :custom
  (blogmore-blogs
   (list
    (blogmore-blog
     :title "blog.davep.org"
     :posts-directory "~/write/davep.github.com/content/posts/"
     :post-subdirectory-function (lambda () (format-time-string "%Y/%m/")))
    (blogmore-blog
     :title "seen-by.davep.dev"
     :posts-directory "~/write/seen-by/content/posts/")))
  :bind
  ("<f12> b" . blogmore))

There's a bunch of other changes and tweaks under the hood in this release too. All of these should come together to make blogmore.el a little more configurable than it was before. I think, to get the best out of it, anyone wanting to configure it "just so" for their purposes will still have to do a little bit of work, which makes me want to spend some time soon writing some proper documentation, complete with examples of how you might achieve different things.

One big change I've made under the hood is to the code that is used when you insert a link to a post (blogmore-link-post). When this runs it lets me pick a file in your filesystem that is a post from my currently-active blog. Once it has the filename it needs to turn it into a root-relative link. So this:

~/write/davep.github.com/content/posts/2026/04/01/2026-04-01-foo.md

needs to become:

/2026/04/01/foo.html

Until now I just did some regexp faffing that took the 2026-04-01- at the start of the filename and swapped each - for /. Nice and easy. Simple enough to code up and get things working a few weeks back. Not at all flexible.

So as a proof-of-concept of how sophisticated someone could get with configuring this I've changed blogmore-default-post-maker-function from this:

(defcustom blogmore-default-post-maker-function
  (lambda (file)
    (replace-regexp-in-string
     (rx bos (group (+ digit)) "-" (group (+ digit)) "-" (group (+ digit)) "-")
     "\\1/\\2/\\3/"
     (file-name-base (file-name-sans-extension file))))
  "Default function to generate a link for a blog post from its filename."
  :type 'function
  :group 'blogmore)

and turned it into this:

(defcustom blogmore-default-post-maker-function
  (lambda (file)
    (format
     "%s/%s"
     (format-time-string
      "%Y/%m/%d"
      (with-temp-buffer
        (insert-file-contents-literally file)
        (parse-iso8601-time-string
         (blogmore--clean-time-string (blogmore--get-frontmatter-property "date")))))
     (replace-regexp-in-string
      (rx bol (= 4 digit) "-" (= 2 digit) "-" (= 2 digit) "-")
      ""
      (file-name-base file))))
  "Default function to generate a link for a blog post from its filename."
  :type 'function
  :group 'blogmore)

So whereas before I was simply messing with the file's name, now I'm loading the date frontmatter from the chosen file and using that to create the date portion of the path in the URL that I use on my blog. The benefit here is that someone might want to keep the date portion of the path in the URL, but never want it as part of the Markdown source file's name, and so this change means they can call the file anything they want; it doesn't look at that but instead uses the actual date from the post's frontmatter.

I think this nicely demonstrates that, especially thanks to how powerful Emacs and Emacs Lisp are, it's fairly easy to make blogmore.el work just the way you want. I think I've provided almost all the hooks necessary to configure it all sorts of ways, but if you do happen to use it and run into something I might have missed, let me know.


  1. I have at least two slightly different date formats going on in my Markdown and Emacs' parse-iso8601-time-string is kind of picky. So I added a function to try and clean that up

-1:-- blogmore.el v3.1 (Post Dave Pearson)--L0--C0--2026-04-05T19:04:44.000Z

Irreal: Repeat Mode

As many you know, I was a Vim user for a couple of decades before I found the one true editor. One of the things I miss from Vim is the easy to use repeat command. If you’re in command mode and perform some command, you can repeat that command by simply pressing .. Sadly, Emacs doesn’t have anything similar. There are a couple of repeat commands: one for simple commands and another for “complex” commands but I was never able to internalize them.

Happily, there is repeat mode that allows you to repeat a command by omitting the prefix and pressing the last key. For example, if you want to repeat the command to enlarge a window horizontally (Ctrl+x }) you can simply repeat the } as many times as needed.

As Bozhidar Batsov explains, this doesn’t work with every key sequence but it is easy to add the functionality to multikey sequences. The TL;DR is that you have to provide a special keymap that maps the last key(s) to their action(s). Batsov has a worked example to show you how to do it. It’s not very hard. But, it turns out, it can be even easier. Omar Antolín explains in a comment that there is also a macro, defvar-keymap that abstracts all the boilerplate away and makes it really easy to define and install a new repeat map.

Repeat mode still isn’t as nice as Vim’s repeat but it can reduce the friction of repeating certain commands. Take a look at Batsov’s post for the details.

Update [2026-04-06 Mon 11:05]:

Karthik Chikmagalur has an excellent write-up on repeat mode that shows it’s useful for much more than simply repeating a command. See his comment below.

-1:-- Repeat Mode (Post Irreal)--L0--C0--2026-04-05T14:31:14.000Z

James Endres Howell: My first advice! (in Emacs Lisp)

It was really fun to learn about advising Lisp functions to extend functionality in Emacs. My first use case was to run a custom function every time a certain function in Bastian Bechtold’s org-static-blog is called. Of course, I could customize that function directly in my own fork, but Lisp advice allows you to modify functions without clobbering them directly. This approach has aesthetic and practical advantages.

But I’ve struggled to understand the concepts and implementation of advice. Today I posted to Mastodon about how excited I was to get my first working use, and Philip asked me to share the code.

My problem was specifying HTML boilerplate that org-static-blog puts in each post when it publishes all the static files, using string variables like org-static-blog-page-header. These strings are complex enough that I put them in their own files, like these lines in the #header.html file that specifies the page metadata:

<script type="module" src="https://esm.sh/emfed@"></script>
<meta name="author" content="James Endres Howell">
<meta name="referrer" content="no-referrer">
<link href="static/style.css" rel="stylesheet" type="text/css" />
<meta name="fediverse:creator" content="@jameshowell@fediscience.org">
<meta property="og:image" 
      content="https://jamesendreshowell.com/static/education-of-james-endres-howell.png">

First, Stack Exchange and I solved the problem of reading a file into a string. (?! How is this not a native function!? Maybe I missed something obvious.)

(defun jeh/file-to-string (file)
  "Return a string that is the contents of FILE."
  (with-temp-buffer
    (insert-file-contents file)
    (buffer-string)))

And then, for example:

(setq org-static-blog-page-header
      (jeh/file-to-string 
       (expand-file-name "#header.html" org-static-blog-template-blocks-directory)))

The unscratched itch was that every time I edit one of these files, I always, but always, forget to update the appropriate variable with the contents of the file! And so publishing doesn’t reflect the changes, and I get confused, and then I remember….

Here is the solution:

(defun jeh/org-static-blog-read-templates (&rest ignore)
  "Set org-static-blog-page -header, -preamble, -postamble variables
by reading files from `org-static-blog-template-blocks-directory'."
  (setq org-static-blog-page-header             ;;; HTML to put in the <head> of each page.
        (jeh/file-to-string 
         (expand-file-name "#header.html" org-static-blog-template-blocks-directory)))
  (setq org-static-blog-page-preamble           ;;; HTML to put before the content of each page.
        (jeh/file-to-string 
         (expand-file-name "#preamble.html" org-static-blog-template-blocks-directory)))
  (setq org-static-blog-page-postamble          ;;; HTML to put after the content of each page.
        (format (jeh/file-to-string 
                 (expand-file-name "#postamble.html" org-static-blog-template-blocks-directory))
                (number-to-string emacs-major-version)
                (number-to-string emacs-minor-version)
                org-version)))

;;; Re-read the template files before publishing,
;;; so changes will be included in output.
(advice-add #'org-static-blog-publish :before #'jeh/org-static-blog-read-templates)

The function jeh/org-static-blog-read-templates sets the variables org-static-blog-page-header, org-static-blog-page-preamble, and org-static-blog-page-postamble to the contents of the appropriate files. Making that function a hook to the function which generates all the static pages, org-static-blog-publish, solves my problem. But there is no hook for it! I had a suspicion that add-advice could give me the same result, and—I hope you’re sitting down—I read the fine manual and learned that the syntax of the last line accomplishes that very thing.

Of course, advice-add (and related functions) can do much more! Maybe as I learn I will be able to customize functions from other packages without just banging on a local fork.

-1:-- My first advice! (in Emacs Lisp) (Post James Endres Howell)--L0--C0--2026-04-04T22:41:00.000Z

Dave Pearson: blogmore.el v2.7

There's no question that the experiment that is BlogMore has resulted in me blogging more. Although my previous setup wasn't exactly all friction, there's something about "owning" most of the tools and really knowing how they work, and being able to quickly modify them so they work "just so", that makes me more inclined to quickly write something up.

I can see this if I look at the numbers in the archive for this blog. In March alone I wrote 43 posts; that's more than I wrote in any whole year, other than 2023. While I suspect this will start to calm down as work on BlogMore and blogmore.el settles down, I sense I'll be writing a bit more often for some time to come.

Because of this I decided to do a little bit of housekeeping on the posts directory in the blog's source repository. Originally I had the Markdown source for every post all in one directory. Then last month I broke those down by year. Then earlier today, seeing how this year is going, I decided to break 2026 down by month.

Then I realised I had a problem in blogmore.el. It assumed that the Markdown file for a new post (blogmore-new) would always be created in a subdirectory named after the year, underneath the defined posts directory. Until today that was the case1, but now I wanted it to work differently.

So this is why I'm making a second release in one day: I added the ability to configure the subdirectory where a new post is created. I've changed the default now so that it assumes the user wants the subdirectory to be YYYY/MM/DD (because more granular feels like the right default). In my case I don't want that, I just want YYYY/MM, but now I can configure that. The value that is set is a function that returns the name of the subdirectory, so in the case of my blog I have it as:

(lambda () (format-time-string "%Y/%m/"))

On the other hand, for my photoblog I want the full date as a subdirectory so I can leave it as the default. The whole use-package for blogmore now looks like:

(use-package blogmore
  :ensure t
  :defer t
  :vc (:url "https://github.com/davep/blogmore.el" :rev :newest)
  :init
  (add-hook 'blogmore-new-post-hook #'end-it)
  (blogmore-work-on "blog.davep.org")
  :custom
  (blogmore-blogs
   '(("blog.davep.org"
      ;; Root directory for posts.
      "~/write/davep.github.com/content/posts/"
      ;; Subdirectory for new posts, relative to the root.
      (lambda () (format-time-string "%Y/%m/")))
     ("seen-by.davep.dev"
      ;; Root directory for posts.
      "~/write/seen-by/content/posts/")))
  :bind
  ("<f12> b" . blogmore))

Technically this is a breaking change because it bumps the meaning of each "position" in the values within blogmore-blogs. However, in my case, because I was only ever defining the blog name and the top-level directory for the posts (both mandatory), this didn't break anything; I also strongly suspect nobody else is using this so I very much doubt I'm messing with someone else's setup2. If I have I apologise; do let me know.

Anyway, all of this goes to explain why the heck I made two releases of the same package back to back in the same day. This is what happens when my namesake is having fun outside and so I just want to sit on the sofa, hack on some code, and watch the chaos in the garden.


  1. For my blog, which again shows that blogmore.el started as a quick hack for getting work done on my blog, but I also want to make it as configurable as possible. 

  2. Even if someone else is using this I would suspect they hadn't configured anything more than I have. 

-1:-- blogmore.el v2.7 (Post Dave Pearson)--L0--C0--2026-04-04T18:24:39.000Z

Dave Pearson: blogmore.el v2.6

Like most people, I imagine, I first ran into transient when first using magit. I took to it pretty quickly and it's always made sense to me as a user interface. But... I've never used it for any code I've ever written.

I think, incorrectly, I'd half assumed it was going to be some faff to set up, and of course for a good while it wasn't part of Emacs anyway. Given this, I'd always had it filed under the heading "that's so neat I'll give it a go one day but not at the moment".

Meanwhile... ever since I did the last big revamp of my Emacs configuration, I found myself leaning into a command binding approach that does the whole prefix-letter-letter thing. For reasons I can't actually remember I fell into the habit of using F121 as my chosen prefix key. As such, over the past 10 or so years (since I greatly overhauled my Emacs setup), I've got into setting up bindings for commands that follow this prefix convention.

So when I created blogmore.el I set up the commands following this pattern.

(use-package blogmore
  :ensure t
  :defer t
  :vc (:url "https://github.com/davep/blogmore.el" :rev :newest)
  :init
  (add-hook 'blogmore-new-post-hook #'end-it)
  (blogmore-work-on "blog.davep.org")
  :custom
  (blogmore-blogs
   '(("blog.davep.org" "~/write/davep.github.com/content/posts/")
     ("seen-by.davep.dev" "~/write/seen-by/content/posts/")))
  :bind
  ("<f12> b b" . blogmore-work-on)
  ("<f12> b n" . blogmore-new)
  ("<f12> b e" . blogmore-edit)
  ("<f12> b d" . blogmore-toggle-draft)
  ("<f12> b s c" . blogmore-set-category)
  ("<f12> b a t" . blogmore-add-tag)
  ("<f12> b u d" . blogmore-update-date)
  ("<f12> b u m" . blogmore-update-modified)
  ("<f12> b l p" . blogmore-link-post)
  ("<f12> b l c" . blogmore-link-category)
  ("<f12> b l t" . blogmore-link-tag))

It works well, it makes it nice and easy to remember the bindings, etc. Nobody needs me to sell them on the merits of this approach.

Then I got to thinking last night: why am I setting up all those bindings when I could probably do it all via a transient? So that was the moment to actually RTFM and get it going. The first version was incredibly quick to get up and running and I was kicking myself that I'd taken so long to actually look at the package properly.

This morning I've worked on it a little more and the final form is still pretty straightforward.

(transient-define-prefix blogmore ()
  "Show a transient for BlogMore commands."
  [:description
   (lambda ()
     (format "BlogMore: %s\n"
             (if (blogmore--chosen-blog-sans-error)
                 (blogmore--blog-title)
               "No blog selected")))
   ["Blog"
    ("b"  "Select blog" blogmore-work-on)]
   ["Post"
    ("n" "New post" blogmore-new :inapt-if-not blogmore--chosen-blog-sans-error)
    ("e" "Edit post" blogmore-edit :inapt-if-not blogmore--chosen-blog-sans-error)
    ("d" "Toggle draft status" blogmore-toggle-draft :inapt-if-not blogmore--blog-post-p)
    ("c" "Set post category" blogmore-set-category :inapt-if-not blogmore--blog-post-p)
    ("t" "Add tag" blogmore-add-tag :inapt-if-not blogmore--blog-post-p)
    ("u d" "Update date" blogmore-update-date :inapt-if-not blogmore--blog-post-p)
    ("u m" "Update modified date" blogmore-update-modified :inapt-if-not blogmore--blog-post-p)]
   ["Links"
    ("l c" "Link to a category" blogmore-link-category :inapt-if-not blogmore--blog-post-p)
    ("l p" "Link to a post" blogmore-link-post :inapt-if-not blogmore--blog-post-p)
    ("l t" "Link to a tag" blogmore-link-tag :inapt-if-not blogmore--blog-post-p)]])

With this in place I can simplify my use-package quite a bit, just binding a single key to run blogmore.

(use-package blogmore
  :ensure t
  :defer t
  :vc (:url "https://github.com/davep/blogmore.el" :rev :newest)
  :init
  (add-hook 'blogmore-new-post-hook #'end-it)
  (blogmore-work-on "blog.davep.org")
  :custom
  (blogmore-blogs
   '(("blog.davep.org" "~/write/davep.github.com/content/posts/")
     ("seen-by.davep.dev" "~/write/seen-by/content/posts/")))
  :bind
  ("<f12> b" . blogmore))

Now, when I'm working on a blog post, I can just hit F12 b and I get a neat menu:

BlogMore with all commands available

Better still, because of how transient works, I can ensure that only applicable commands are available, while still showing them all. So if I've not even got a blog selected yet:

With no commands available

Or with a blog selected but not actually working on a post yet:

With some commands available

So far I'm really liking this approach, and I'm tempted to lean into transient more with some of my packages now. While on the surface it does seem that it has the downside of the binding choices being dictated by me, the fact is that the commands are all still there and anyone can use their own bindings, or I guess override the transient itself and do their own thing.


  1. Yes, it is a bit out of the way on the keyboard, but so is Esc. I find my muscle memory has no problem with it. 

-1:-- blogmore.el v2.6 (Post Dave Pearson)--L0--C0--2026-04-04T10:48:21.000Z

Emacs Redux: Declutter M-x with read-extended-command-predicate

This is another article inspired by my recent cleanup of Prelude and my personal Emacs config, following the one on repeat-mode. I’ve been going through the Emacs 28-30 changelogs looking for features I had overlooked, and this small one from Emacs 28 turned out to be a real gem.

Ever noticed how M-x shows you every command, including ones that make no sense in your current buffer? Org commands while editing Ruby, Magit commands in a shell buffer, that sort of thing. It’s not a huge deal if you know what you’re looking for, but it adds noise to the candidate list – especially if you’re using a completion framework like Vertico or Ivy that shows everything at a glance.

Emacs 28 added a simple way to fix this:

(setq read-extended-command-predicate
      #'command-completion-default-include-p)

With this setting, M-x hides commands that declare themselves inapplicable to the current major mode from the completion candidates. So if you’re in a Python buffer, you won’t see dired-do-rename or clojure-align cluttering your results.

How does the filtering actually work? command-completion-default-include-p looks at the modes a command declares it belongs to (via the interactive form) or checks its completion-predicate symbol property. If no modes are declared and there’s no completion predicate, the command is included as usual – so existing commands that haven’t been updated are not affected.

Emacs actually ships with three predicates you can choose from (plus nil for no filtering):

  • command-completion-default-include-p – the safe default. Excludes commands tagged for other modes, includes everything else.
  • command-completion-using-modes-and-keymaps-p – more aggressive. Shows commands tagged for the current mode plus any command that has a keybinding in the buffer’s active keymaps. Also always includes customize-* commands. Untagged commands without keybindings are hidden.
  • command-completion-using-modes-p – the strictest option. Only shows commands explicitly tagged for the current mode. Untagged commands are hidden too, so this can be quite aggressive.

I’d recommend starting with command-completion-default-include-p since it’s the most conservative – it won’t hide anything that hasn’t explicitly opted in to the filtering.

Package authors can declare mode affiliation by adding a mode specification to the interactive form:

(defun my-foo-command ()
  "Do something useful in foo-mode."
  (interactive nil foo-mode)
  ...)

The nil is the interactive spec (no arguments in this case), and foo-mode tells Emacs this command is only relevant in foo-mode buffers. If a command applies to multiple modes, just list them all:

(defun cider-eval-defun-at-point ()
  "Evaluate the top-level form at point."
  (interactive nil clojure-mode clojure-ts-mode)
  ...)

This is handy for packages like CIDER that need to work in both the classic clojure-mode and the newer Tree-sitter-based clojure-ts-mode.

As for how well this works in practice – many built-in commands already declare their applicable modes, so you’ll see a noticeably cleaner M-x right away. Third-party package adoption is growing but uneven. Commands that haven’t been updated will simply continue to show up everywhere, same as before – so there’s no downside to enabling this.

A Note for Vertico/Orderless Users

If you followed the Vertico sample configuration, you’ll find this setting already there – commented out. It was shipped that way because it was new at the time and some users found the disappearing commands surprising. It’s been stable for years now and works great with Vertico, Orderless, and Marginalia. Just uncomment it and enjoy a less noisy M-x.

Commands that are filtered out aren’t gone – the filtering only affects completion candidates. If you type the full command name at the M-x prompt it will still execute just fine.

That’s all I have for you today. Keep hacking!

-1:-- Declutter M-x with read-extended-command-predicate (Post Emacs Redux)--L0--C0--2026-04-04T08:00:00.000Z

Emacs Redux: Repeat Mode: Stop Repeating Yourself

I’ve been going through the Emacs 28-30 changelogs recently as part of a big update to Prelude and my personal config, looking for features I never got around to trying. repeat-mode is one I wish I’d adopted sooner.

How many times have you typed C-x o C-x o C-x o to cycle through a few windows? Or C-x { C-x { C-x { to keep shrinking one? All that prefix repetition is pure friction.

repeat-mode is a built-in minor mode (Emacs 28+) that lets you drop the prefix after the first invocation and just keep pressing the final key. Enable it with one line:

(repeat-mode 1)

One important thing to understand – this doesn’t magically work for every key sequence. A command is only “repeatable” if it has been explicitly added to a repeat map. Emacs ships with repeat maps for a bunch of common built-in commands, though, so you get a decent experience out of the box. Here are some of the highlights:

  • C-x o o o – keep cycling windows
  • C-x { { { / C-x } } } – shrink/grow window horizontally
  • C-x ^ ^ ^ – grow window vertically
  • C-x u u u – keep undoing
  • C-x <left> <left> / C-x <right> <right> – cycle through buffer history
  • M-g n n n / M-g p p p – jump through next-error results

The transient state ends as soon as you press any key that isn’t part of the repeat map.

If you’d prefer it to time out automatically, there’s a setting for that:

(setq repeat-exit-timeout 5) ;; exit after 5 seconds of inactivity

Defining Your Own Repeat Maps

The real power comes from defining repeat maps for your own commands. For instance, if you use expreg for expand-region, you can set things up so that C-= = = = - expands three times then contracts once:

(defvar expreg-repeat-map
  (let ((map (make-sparse-keymap)))
    (define-key map "=" #'expreg-expand)
    (define-key map "-" #'expreg-contract)
    map))

(put 'expreg-expand 'repeat-map 'expreg-repeat-map)
(put 'expreg-contract 'repeat-map 'expreg-repeat-map)

The pattern is simple: create a keymap, then attach it to the relevant commands via the repeat-map symbol property. Any command with that property becomes “repeatable” after first invocation.

That’s all there is to it. One line to enable, and a lot less C-x mashing in your future.

Are you using repeat-mode? Have you defined any custom repeat maps that you find particularly useful? I’d love to hear about them in the comments!

Keep hacking!

-1:-- Repeat Mode: Stop Repeating Yourself (Post Emacs Redux)--L0--C0--2026-04-04T07:00:00.000Z

Protesilaos Stavrou: Emacs live stream with Sacha Chua on 2026-04-16 17:30 Europe/Athens

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

The other day I had a coaching session with Sacha Chua. Sacha asked me if she could record and publish it, to which I agreed. More here: https://sachachua.com/blog/2026/04/yayemacs-10-emacs-coaching-with-prot-packaging-emacs-lisp//.

Our next meeting will be done live on the 16th of April 2026 at 10:30 America/Toronto, 17:30 Europe/Athens time: https://youtube.com/live/djE_pVlgDHg.

I will check with Sacha how she imagines doing this. Though I am the laissez faire type, so will adapt as we go.

[ Note that all my coaching sessions are private: I never share details of my meetings. This is an exception because Sacha asked me about it. ]

-1:-- Emacs live stream with Sacha Chua on 2026-04-16 17:30 Europe/Athens (Post Protesilaos Stavrou)--L0--C0--2026-04-04T00:00:00.000Z

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