<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>notes from /dev/null - emacs</title><link href="http://yummymelon.com/devnull/" rel="alternate"/><link href="http://yummymelon.com/devnull/feeds/tags/emacs.atom.xml" rel="self"/><id>http://yummymelon.com/devnull/</id><updated>2026-08-24T14:00:00-07:00</updated><entry><title>Announcing shazam.el</title><link href="http://yummymelon.com/devnull/announcing-shazam-el.html" rel="alternate"/><published>2026-08-24T14:00:00-07:00</published><updated>2026-08-24T14:00:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-08-24:/devnull/announcing-shazam-el.html</id><summary type="html">&lt;p&gt;Announcing an integration between Emacs and the Shazam music recognition service on macOS, &lt;code&gt;shazam.el&lt;/code&gt;.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="img" src="images/announcing-shazam/shazam-screenshot.png"&gt;&lt;/p&gt;
&lt;p&gt;Continuing my explorations on malleable computing with Emacs is examining access to OS/Platform-specific resources, in this case with macOS. macOS provides a number of mechanisms for high-level &lt;a href="https://en.wikipedia.org/wiki/Inter-process_communication"&gt;inter-process communication&lt;/a&gt;, among them AppleScript &amp;amp; Automator Actions (now considered legacy but still maintained) and App Intents, Entities &amp;amp; Shortcuts (introduced in macOS 13 and considered by Apple a replacement for the former).&lt;/p&gt;
&lt;p&gt;This post both announces and covers the implementation of an Emacs integration with the Shazam music recognition service that is packaged with macOS. This Emacs integration is called &lt;code&gt;shazam.el&lt;/code&gt; (&lt;a href="https://github.com/kickingvegas/shazam"&gt;https://github.com/kickingvegas/shazam&lt;/a&gt;) and is now available for installation via &lt;a href="https://melpa.org/#/shazam"&gt;MELPA&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To understand the &lt;code&gt;shazam.el&lt;/code&gt; implementation, the following background information is provided.&lt;/p&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;Shazam is a music recognition service that is implemented across Apple's different platforms (macOS, iOS/iPadOS, watchOS, tvOS). This service provides a shortcut action (&lt;em&gt;Recognize Music&lt;/em&gt;) which is implemented using the following APIs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;App Intent:&lt;/strong&gt; An expression of an app’s capabilities to the system (in this case the ecosystem of Cocoa-based applications), and contains code to perform that action (&lt;a href="https://developer.apple.com/documentation/appintents/app-intents"&gt;App intents | Apple Developer Documentation&lt;/a&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;App Entity:&lt;/strong&gt; Information provided to the system about your app’s data, or about concepts related to your app’s data (&lt;a href="https://developer.apple.com/documentation/appintents/app-entities"&gt;App entities | Apple Developer Documentation&lt;/a&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;App Shortcut:&lt;/strong&gt; A workflow (including UI) for orchestrating &lt;em&gt;App Intents&lt;/em&gt; and &lt;em&gt;Entities&lt;/em&gt; (&lt;a href="https://developer.apple.com/documentation/appintents/app-shortcuts"&gt;App Shortcuts | Apple Developer Documentation&lt;/a&gt;). There are a number of ways to access a Shortcut, among them via the Shortcuts app or Siri.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With the &lt;em&gt;Recognize Music&lt;/em&gt; shortcut action we can realize the Elisp package &lt;code&gt;shazam.el&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Implementation&lt;/h1&gt;
&lt;p&gt;A Shortcut (aka workflow) defined in the Shortcuts app can be invoked via command line as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;shortcuts&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;shortcut name&amp;gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;where “shortcut name” is the name given to a Shortcut enclosed by double-quotes.&lt;/p&gt;
&lt;p&gt;If the Shortcut is intended to write to &lt;code&gt;stdout&lt;/code&gt;, then the &lt;code&gt;shortcuts&lt;/code&gt; command line utility must be run into a pipe.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;shortcuts&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;shortcut name&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cat
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The above incantation lets Emacs run a Shortcut.&lt;/p&gt;
&lt;p&gt;The Shortcut used by &lt;code&gt;shazam.el&lt;/code&gt; invokes the &lt;em&gt;Recognize Music&lt;/em&gt; action and does some work to transform the Shazam search result into a JSON dictionary. The JSON dictionary is written to &lt;code&gt;stdout&lt;/code&gt; (&lt;a href="https://www.icloud.com/shortcuts/bba3dd21146c4ba78dff1d7d0c0b1092"&gt;iCloud Shortcut: “Identify Music JSON”&lt;/a&gt;) which can be read by Emacs and processed.&lt;/p&gt;
&lt;p&gt;The sequence diagram below shows the overall integration workflow between Emacs and Shortcuts for &lt;code&gt;shazam.el&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/announcing-shazam/shazam-arch.svg"&gt;&lt;/p&gt;
&lt;h1&gt;Features&lt;/h1&gt;
&lt;p&gt;Details on all the features provided by &lt;code&gt;shazam.el&lt;/code&gt; can be found in its &lt;a href="https://kickingvegas.github.io/shazam/"&gt;User Guide&lt;/a&gt;. Of note is that once the Shazam search result is deserialized, its content can be processed into a variety of formats. &lt;code&gt;shazam.el&lt;/code&gt; takes advantage of this by storing recognized search results into an Org file, using Org markup and properties to store metadata provided by each result. Users can subsequently peruse this history file at their convenience.&lt;/p&gt;
&lt;h1&gt;Closing Thoughts&lt;/h1&gt;
&lt;p&gt;This work builds off the explorations in malleable computing with Emacs described in my prior posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://yummymelon.com/devnull/in-emacs-everything-looks-like-a-service.html"&gt;nfdn: In Emacs, Everything Looks Like a Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://yummymelon.com/devnull/malleable-computing-emacs-and-you.html"&gt;nfdn: Malleable Computing, Emacs, and You&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://yummymelon.com/devnull/announcing-now-playing-el.html"&gt;nfdn: Announcing now-playing.el, an Emacs interface for the macOS Music app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is not a lot of Elisp to &lt;a href="https://github.com/kickingvegas/shazam/blob/0e5a97c59b5972a728138fc06b2ff371d42c8b55/lisp/shazam.el"&gt;shazam.el&lt;/a&gt; as shown in the &lt;code&gt;cloc&lt;/code&gt; result for it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;github&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;com/AlDanial/cloc v 2&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;10  T=0&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;01 s (143&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;3 files/s&lt;/span&gt;&lt;span class="nt"&gt;,&lt;/span&gt;&lt;span class="c"&gt; 35402&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;8 lines/s)&lt;/span&gt;
&lt;span class="nb"&gt;-------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;Language                     files          blank        comment           code&lt;/span&gt;
&lt;span class="nb"&gt;-------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;Lisp                             1             50             38            159&lt;/span&gt;
&lt;span class="nb"&gt;-------------------------------------------------------------------------------&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most of the challenge in building &lt;code&gt;shazam.el&lt;/code&gt; was understanding how &lt;code&gt;start-process&lt;/code&gt; worked to asynchronously run the &lt;code&gt;shortcuts&lt;/code&gt; command line utility.&lt;/p&gt;
&lt;p&gt;A downside to using a Shortcut (workflow) is the requirement to use a binary code-signed representation that is accessed via the Shortcuts app. This makes it less flexible than AppleScript where declaring behavior can be done in plain text. For example, the &lt;code&gt;now-playing.el&lt;/code&gt; package can remote-control the Music app via AppleScript that is directly invoked from Emacs built with support for &lt;code&gt;ns-do-applescript&lt;/code&gt;. In contrast, accessing contemporary macOS app behavior that is exposed via Intents and Entities requires installing (or creating) a binary code-signed representation (the Shortcut). This adds an extra step (or steps for installing multiple shortcuts) to build an integration with Emacs or any other orchestration tool.&lt;/p&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/kickingvegas/shazam"&gt;shazam.el Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/automator"&gt;Automator | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/AppIntents"&gt;App Intents | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/appintents/app-entities"&gt;App Entities | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/appintents/app-shortcuts"&gt;App Shortcuts | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="misc"/><category term="emacs"/><category term="org mode"/><category term="macos"/></entry><entry><title>Simplifying Setup for Casual</title><link href="http://yummymelon.com/devnull/simplifying-setup-for-casual.html" rel="alternate"/><published>2026-08-18T09:20:00-07:00</published><updated>2026-08-18T09:20:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-08-18:/devnull/simplifying-setup-for-casual.html</id><summary type="html">&lt;p&gt;Casual v3.0 update is now available on MELPA and NonGNU ELPA with a vastly simplified setup process.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Since its inception, &lt;a href="https://kickingvegas.github.io/casual/"&gt;Casual&lt;/a&gt; required individual setup for each module it supported. This seemed fine at the start when the module count was low. Now weighing in at &lt;a href="https://kickingvegas.github.io/casual/Casual-Modules.html"&gt;26 modules&lt;/a&gt;, there is admittedly a &lt;em&gt;lot&lt;/em&gt; of setup.&lt;/p&gt;
&lt;p&gt;Not anymore. Starting with the latest &lt;a href="https://github.com/kickingvegas/casual/discussions/495"&gt;Casual v3.0 update&lt;/a&gt;, there is now a convenience command to setup all Casual modules: &lt;code&gt;casual-init&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use the following Elisp to replace all the previous setup code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;#39;casual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;casual-init&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can configure which modules to setup via the customizable hook variable &lt;code&gt;casual-init-hook&lt;/code&gt;, a list of hook functions each associated with a module. Having a module hook function present in &lt;code&gt;casual-init-hook&lt;/code&gt; tells &lt;code&gt;casual-init&lt;/code&gt; to setup that module.&lt;/p&gt;
&lt;p&gt;Editing &lt;code&gt;casual-init-hook&lt;/code&gt; is straightforward using the customize checkbox interface shown below.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/simplifying-casual/casual-init-hook-customization.png"&gt;&lt;/p&gt;
&lt;p&gt;More information about installation and setup of Casual can be found in the &lt;a href="https://kickingvegas.github.io/casual/Install.html"&gt;Casual User Guide&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Bumping to 3.0&lt;/h1&gt;
&lt;p&gt;Because the recommended setup has significantly changed, I’ve decided to bump the major version to 3.0.&lt;/p&gt;
&lt;p&gt;That said, there are no functional changes to Casual and all the prior individual setup still should work without change. For existing users of Casual, I highly recommend migrating to &lt;code&gt;casual-init&lt;/code&gt; as it allows for removing individual setup code.&lt;/p&gt;
&lt;p&gt;A significant amount of manual testing was required to verify lazy-loading behavior in &lt;code&gt;casual-init&lt;/code&gt;. While best effort was made to avoid unpleasant surprise, if something shows up, please &lt;a href="https://github.com/kickingvegas/casual/issues"&gt;file an issue&lt;/a&gt; to let me know.&lt;/p&gt;
&lt;p&gt;Casual v3.0 update is now available on &lt;a href="https://melpa.org/#/casual"&gt;MELPA&lt;/a&gt; and &lt;a href="https://elpa.nongnu.org/nongnu/casual.html"&gt;NonGNU ELPA&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It costs money to make, enhance, and maintain Casual as ideologically free software. If you enjoy using Casual, please consider &lt;a href="https://www.buymeacoffee.com/kickingvegas"&gt;buying me a coffee&lt;/a&gt; to help support its development and maintenance. Thank you!&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="casual"/></entry><entry><title>Announcing now-playing.el, an Emacs interface for the macOS Music app</title><link href="http://yummymelon.com/devnull/announcing-now-playing-el.html" rel="alternate"/><published>2026-08-03T10:55:00-07:00</published><updated>2026-08-03T10:55:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-08-03:/devnull/announcing-now-playing-el.html</id><summary type="html">&lt;p&gt;Announcing a remote control interface for the macOS Music app from Emacs, keeping in tradition of never leaving it.&lt;/p&gt;</summary><content type="html">&lt;p&gt;For me, listening to music whenever I’m working on a computer is a commonplace, daresay essential activity. Much of my work is done using Emacs, so the idea of having it control music playback to mitigate an app switch seemed reasonable. Such was the motivation for my latest project, &lt;code&gt;now-playing.el&lt;/code&gt;, a &lt;a href="https://docs.magit.vc/transient/"&gt;Transient&lt;/a&gt; interface for the macOS Music (nee iTunes) app.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/announcing-now-playing/now-playing-screenshot.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;now-playing.el&lt;/code&gt; takes inspiration from the many mini-client interfaces provided by Apple to control the Music app. The idea was to have a compact and simple interface:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Show what is currently playing (song, artist, album)&lt;/li&gt;
&lt;li&gt;Offer basic controls (play/pause, next/previous track, volume control)&lt;/li&gt;
&lt;li&gt;Optionally log what was played.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;now-playing.el&lt;/code&gt; takes advantage of AppleScript support in the Music app, providing the needed controllability and observability of it from Emacs. The sequence diagram below illustrates how these programs relate to each other by showing a request/response transaction between Emacs and the Music app using the &lt;code&gt;osascript&lt;/code&gt; command line tool.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/announcing-now-playing/now-playing-arch.svg"&gt;&lt;/p&gt;
&lt;p&gt;If Emacs is built with &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Mac-OS-_002f-GNUstep.html"&gt;NextStep (NS) support&lt;/a&gt;, then Emacs can send a direct AppleScript message to the Music app via &lt;code&gt;ns-do-applescript&lt;/code&gt; to avoid making a shell call to &lt;code&gt;osascript&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/announcing-now-playing/now-playing-direct-arch.svg"&gt;&lt;/p&gt;
&lt;p&gt;State synchronization for &lt;code&gt;now-playing.el&lt;/code&gt; is fairly simple, with client updates done in a pull fashion. More details about what &lt;code&gt;now-playing.el&lt;/code&gt; can do can be found in the &lt;a href="https://kickingvegas.github.io/now-playing/"&gt;now-playing.el User Guide&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Closing Thoughts&lt;/h1&gt;
&lt;p&gt;It is not coincidental that writing this mini-client for the Music app works into my &lt;a href="http://yummymelon.com/devnull/malleable-computing-emacs-and-you.html"&gt;recent explorations and musings on malleable computing&lt;/a&gt; with Emacs. As of this writing, &lt;code&gt;now-playing.el&lt;/code&gt; weighs in at &lt;a href="https://github.com/kickingvegas/now-playing/blob/main/lisp/now-playing.el"&gt;326 lines of code&lt;/a&gt; (&lt;code&gt;cloc&lt;/code&gt; measured), taking full advantage of Elisp libraries to construct the following client subsystems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UI: Transient for menu, &lt;code&gt;cus-edit.el&lt;/code&gt; for customization UI&lt;/li&gt;
&lt;li&gt;Client Edge: &lt;code&gt;shell-command-to-string&lt;/code&gt; to &lt;code&gt;osascript&lt;/code&gt;, &lt;code&gt;ns-do-applescript&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Local Database: Elisp variables&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;now-playing.el&lt;/code&gt; explores integration with &lt;a href="https://en.wikipedia.org/wiki/AppleScript"&gt;AppleScript&lt;/a&gt;, itself a mechanism to allow for orchestration between &lt;a href="https://en.wikipedia.org/wiki/Cocoa_(API)"&gt;Cocoa applications&lt;/a&gt;. Expect some future posts documenting my explorations in this area, particularly with the more contemporary Apple Shortcuts and Intents.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;now-playing.el&lt;/code&gt; v1.0 is &lt;a href="https://melpa.org/#/now-playing"&gt;now available on MELPA&lt;/a&gt;.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="macos"/></entry><entry><title>Malleable Computing, Emacs, and You</title><link href="http://yummymelon.com/devnull/malleable-computing-emacs-and-you.html" rel="alternate"/><published>2026-07-22T14:00:00-07:00</published><updated>2026-07-22T14:00:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-07-22:/devnull/malleable-computing-emacs-and-you.html</id><summary type="html">&lt;p&gt;Some musings on malleable computing using as an example a GitHub issues browser in Emacs.&lt;/p&gt;</summary><content type="html">&lt;p&gt;It all started with a routine task. I use GitHub issues for all of my public-facing projects but my preference is keeping track of things in Org Agenda. To reconcile the two, I would manually copy over the GitHub issue to an Org file, typically the title and description, &lt;em&gt;like an animal&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Despite this separation, the duplicated issue allowed me to treat it as a scratchpad for anything I could express in Org. In this way, I used the duplicated issue in Org as both a dedicated area to take notes and a “staging” area to compose follow-up comments that I’d want to share in the public facing issue.&lt;/p&gt;
&lt;p&gt;I manually copied for far longer than I care to admit. Repeat a task enough times in Emacs and the inevitable thought arises: “I should automate this.”&lt;/p&gt;
&lt;p&gt;This post recounts how I automated this task and in doing so, highlight the malleable computing capabilities of Emacs. It should also be considered a follow-up to my earlier post “&lt;a href="http://yummymelon.com/devnull/in-emacs-everything-looks-like-a-service.html"&gt;In Emacs, Everything Looks Like a Service&lt;/a&gt;”.&lt;/p&gt;
&lt;h1&gt;Requirements&lt;/h1&gt;
&lt;p&gt;An essential question to ask in any automation exercise is “What do I want done?”&lt;/p&gt;
&lt;p&gt;I wanted to be able to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Easily copy a GitHub issue (title, description, some metadata) as an Org task that can be tracked in an Agenda view.&lt;/li&gt;
&lt;li&gt;Work primarily from Emacs to minimize the context switching between it and a web browser.&lt;/li&gt;
&lt;li&gt;Express my thoughts in Org syntax.&lt;/li&gt;
&lt;li&gt;Create a new GitHub issue.&lt;/li&gt;
&lt;li&gt;Avoid dealing with GitHub authentication.&lt;/li&gt;
&lt;li&gt;From Emacs, open a GitHub issue in a web browser.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Another essential question to ask is “What do I &lt;em&gt;not&lt;/em&gt; want to do?”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install or write a full-featured GitHub client.&lt;ul&gt;
&lt;li&gt;Worry too much about synchronization logic between local (Emacs) and server (GitHub) state.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spend a lot of time on this (ideally have something working in a day, no more than a week).&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Specification&lt;/h1&gt;
&lt;p&gt;With the above requirements in place, the next question is “how do I build this?”&lt;/p&gt;
&lt;p&gt;For this particular exercise, I decided to leverage my existing install of the GitHub command line utility &lt;a href="https://cli.github.com"&gt;gh&lt;/a&gt;. The benefits for this are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub authentication is delegated to &lt;code&gt;gh&lt;/code&gt;; no need to mess with it directly from Emacs.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Emacs can treat &lt;code&gt;gh&lt;/code&gt; as a REST service to GitHub as shown in the diagram below&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/malleable-computing-emacs/emacs-gh.svg"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To flesh out the rest of our tool, we can leverage using different Elisp packages and programs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;For the user interface, the &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/transient/"&gt;Transient&lt;/a&gt; and &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/vtable/"&gt;Variable Pitch Table (vtable)&lt;/a&gt; packages are used for menus and display respectively.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For Org to Markdown translation, &lt;a href="https://melpa.org/#/ox-gfm"&gt;ox-gfm&lt;/a&gt; will be used.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For Markdown to Org translation, &lt;a href="https://pandoc.org"&gt;Pandoc&lt;/a&gt; will be used.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Elisp native &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-JSON.html"&gt;JSON&lt;/a&gt; support will be used for deserializing the JSON responses from &lt;code&gt;gh&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Implementation&lt;/h1&gt;
&lt;p&gt;The implementation of the above is published as the package &lt;a href="https://github.com/kickingvegas/gah"&gt;gah&lt;/a&gt; with its source available for examination in the file &lt;a href="https://github.com/kickingvegas/gah/blob/main/lisp/gah.el"&gt;gah.el&lt;/a&gt;. Of note is the function &lt;code&gt;gah-request-issues&lt;/code&gt; which does the work of retrieving GitHub issues via &lt;code&gt;gh&lt;/code&gt; as shown below.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;gah-request-issues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Request issues for REPO.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;gah-browser-fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cmd-list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;gh&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;--repo&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;#39;%s&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;issue&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;list&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;--limit&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;number-to-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;gah-request-issue-count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;--json&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string-join&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;,&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;json-parse-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;shell-command-to-string&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string-join&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cmd-list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="nb"&gt;:null-object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Consider the high degree of abstraction provided by &lt;code&gt;gah-request-issues&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The list &lt;code&gt;cmd-list&lt;/code&gt; forms the request (in this case, the arguments to invoke &lt;code&gt;gh&lt;/code&gt; with).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shell-command-to-string&lt;/code&gt; dispatches the request to &lt;code&gt;gh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The returned JSON response is handled by &lt;code&gt;json-parse-string&lt;/code&gt; to deserialize the JSON into an Elisp hash table.&lt;/li&gt;
&lt;li&gt;All of the above is accomplished in less than 20 lines of code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The returned hash table result is subsequently processed to populate a vtable as shown below. From the vtable, the user can navigate the list of issues with an ancillary window updated to show the details of a selected issue.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/malleable-computing-emacs/gah-screenshot.png"&gt;&lt;/p&gt;
&lt;p&gt;Multiple commands and functions working with said hash-table were made to satisfy the requirements above. They are accessible via the Transient menu shown below:&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/malleable-computing-emacs/gah-transient.png"&gt;&lt;/p&gt;
&lt;h1&gt;Malleable Computing Observations&lt;/h1&gt;
&lt;p&gt;As Elisp is a &lt;a href="https://en.wikipedia.org/wiki/Dynamic_programming_language"&gt;dynamic programming language&lt;/a&gt;, the above function (or some variant of it) can be coded and evaluated within a running Emacs session. In Emacs it is routine practice to prototype code behavior without the need to restart it. Contrast this with tools that are built with a static language and have no extensibility, where the &lt;em&gt;edit-compile-debug&lt;/em&gt; development cycle must be applied to exercise behavior, provided the source code is available.&lt;/p&gt;
&lt;p&gt;Emacs provides numerous ways to edit and evaluate Elisp code, among them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A scratch buffer&lt;/li&gt;
&lt;li&gt;An Elisp file&lt;/li&gt;
&lt;li&gt;An Org source block&lt;/li&gt;
&lt;li&gt;IELM REPL&lt;/li&gt;
&lt;li&gt;Eshell&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eval-expression&lt;/code&gt; (&lt;code&gt;M-:&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because there is no isolation between loaded Elisp code, all of it can be orchestrated together in an improvised fashion. Any program accessible to Emacs via shell further adds to this mix.&lt;/p&gt;
&lt;p&gt;Provided high abstractions, the amount of code needed to be written for desired behavior can be small. At the time of this writing, &lt;code&gt;gah.el&lt;/code&gt; has approximately 400 lines of code has been measured by &lt;code&gt;cloc&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;github&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;com/AlDanial/cloc v 2&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;08  T=0&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;01 s (146&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;4 files/s&lt;/span&gt;&lt;span class="nt"&gt;,&lt;/span&gt;&lt;span class="c"&gt; 76550&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;1 lines/s)&lt;/span&gt;
&lt;span class="nb"&gt;-------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;Language                     files          blank        comment           code&lt;/span&gt;
&lt;span class="nb"&gt;-------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;Lisp                             1             93             38            392&lt;/span&gt;
&lt;span class="nb"&gt;-------------------------------------------------------------------------------&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It took me about 2½ hours to build the basic behavior I wanted (request issues from GitHub, then display) and the rest of the day to cover all the original requirements. Everything after that was just refactoring.&lt;/p&gt;
&lt;p&gt;Motivated readers are invited to examine &lt;code&gt;gah.el&lt;/code&gt; to understand its details. At this point though, I’d like to take the opportunity to muse on software engineering and malleable computing.&lt;/p&gt;
&lt;h2&gt;Software Scope - Some Percentage Anecdotes&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://en.wikipedia.org/wiki/Ninety–ninety_rule"&gt;90/90 rule&lt;/a&gt; posits that “The first 90 percent of the code accounts for the first 90 percent of the development time. The remaining 10 percent of the code accounts for the other 90 percent of the development time”. Closely related is the &lt;a href="https://en.wikipedia.org/wiki/Pareto_principle"&gt;Pareto principle&lt;/a&gt; (aka the 80/20 rule) as applied (or more mis-applied) to software development: only 20% of your features will be actually used by 80% of your users.&lt;/p&gt;
&lt;p&gt;From control theory there is the concept of &lt;a href="https://en.wikipedia.org/wiki/BIBO_stability"&gt;bounded input, bounded output (BIBO) stability&lt;/a&gt;. If the system is BIBO stable, then whatever input that is bounded will have an output that is also bounded.&lt;/p&gt;
&lt;p&gt;Coupling these ideas together, if the functionality you want (BIBO stability) is within that 20% deliverable then you are positioned to get a desired result &lt;em&gt;faster&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Unfortunately, this observation is not available for many producers shipping a tool that must satisfy a large audience. In Sinofsky’s post “&lt;a href="https://hardcoresoftware.learningbyshipping.com/p/077-what-is-bloat-really?s=w"&gt;What is Software Bloat, Really?&lt;/a&gt;”, he describes the product definition issues with shipping Microsoft Office, particularly with feature set. From their user research, they were led to this finding: “… the data was entirely conclusive: Most of Office was used. But no one person used the entire product.”&lt;/p&gt;
&lt;p&gt;When discussing product definition, it is helpful to distinguish two informal dynamics: supply and demand.&lt;/p&gt;
&lt;p&gt;With software that is only supply-provided (such as the case with MS Office), the weight of product definition is shouldered by the producer. If the producer wants to serve a vast audience, it is likely their feature set and corresponding development scope will be vast as well. On the demand-side, consumers can make requests for new features, but their prioritization is controlled by the producer. In this situation, the roles of producer and consumer are strongly delineated.&lt;/p&gt;
&lt;p&gt;Malleable software, where users have agency to adapt and reshape their digital tools, offers a different possibility: producers provide the building blocks and let consumers make their own tools. In this, both existing code and programs are recombined to make new behavior. Stated another way, malleable software takes advantage of combinatorial explosion in that a desired subset of behavior can likely be found within the state space of combining existing libraries (in this case Elisp) with different programs. With malleable software, the roles between producer and consumer are less delineated. A consumer building a new tool out of malleable building blocks now must shoulder the weight of product definition.&lt;/p&gt;
&lt;h2&gt;Build for 1 or N&lt;/h2&gt;
&lt;p&gt;The scope to build for 1 vs &lt;em&gt;N&lt;/em&gt; can be an order of magnitude with the kicker that &lt;em&gt;N&lt;/em&gt; need not be large. Choosing to write code for another person (&lt;em&gt;N=2&lt;/em&gt;) escalates concerns that could otherwise be brushed aside:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Code maintainability&lt;ul&gt;
&lt;li&gt;Modularity/Reuse&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Unit and Integration Testing&lt;/li&gt;
&lt;li&gt;Packaging&lt;/li&gt;
&lt;li&gt;Distribution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both a blessing and curse of malleable techniques is that they allow for “just good enough” capability. Malleable techniques encourage building for 1 as reaching a state of “it works for me” is often sufficient to declare victory.&lt;/p&gt;
&lt;p&gt;For most software that is only supply-provided, building for &lt;em&gt;N&lt;/em&gt; is a requirement. For most software built with malleable techniques, building for &lt;em&gt;N&lt;/em&gt; is a choice.&lt;/p&gt;
&lt;h2&gt;Malleable Software and User Agency&lt;/h2&gt;
&lt;p&gt;Returning back to &lt;code&gt;gah&lt;/code&gt;, the benefits of building for 1 are evident:&lt;/p&gt;
&lt;p&gt;From within Emacs I can now easily:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Peruse GitHub issues for a specified repository.&lt;/li&gt;
&lt;li&gt;Copy a GitHub issue into a Org file.&lt;/li&gt;
&lt;li&gt;Create a new GitHub issue using Org syntax.&lt;/li&gt;
&lt;li&gt;Open a GitHub issue in a web browser.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Implementing &lt;code&gt;gah&lt;/code&gt; in Emacs was straightforward as I could leverage Elisp packages (both built-in and 3rd party) and external applications (&lt;code&gt;gh&lt;/code&gt;, &lt;code&gt;pandoc&lt;/code&gt;) to build it. Within a day I had a tool that did what I wanted. I did not have to ask for permission nor required privileged materials (source code) to build &lt;code&gt;gah&lt;/code&gt;. With Emacs, I could &lt;em&gt;just do it&lt;/em&gt;. Such individual empowerment advanced by malleable techniques is liberating in contrast to working with siloed applications with little to no integration between them.&lt;/p&gt;
&lt;h1&gt;Closing Thoughts&lt;/h1&gt;
&lt;p&gt;This post muses about the malleable computing capabilities provided by Emacs, showing by concrete example (&lt;code&gt;gah.el&lt;/code&gt;) how new behavior can be created leveraging both code and program reuse in an improvised fashion. With measured expectations (requirements, feature set, audience), malleable techniques allow for building tools that would otherwise be infeasible without it.&lt;/p&gt;
&lt;h1&gt;Links&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/kickingvegas/gah"&gt;https://github.com/kickingvegas/gah&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;“&lt;a href="https://hardcoresoftware.learningbyshipping.com/p/077-what-is-bloat-really?s=w"&gt;What Is Software Bloat, Really?&lt;/a&gt;”, Steven Sinofsky.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Addendum 2026-07-23 11:30 PST&lt;/h1&gt;
&lt;p&gt;Doh. Because this started out as a personal project, I did not do any due diligence on the project naming. Turns out the name &lt;code&gt;fj&lt;/code&gt; is already used as a &lt;a href="https://codeberg.org/martianh/fj.el"&gt;client for Forgejo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As such I've renamed my project to &lt;code&gt;gah&lt;/code&gt; and amended this post accordingly. My apologies for any slights this has caused.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="org mode"/></entry><entry><title>In Emacs, Everything Looks Like a Service</title><link href="http://yummymelon.com/devnull/in-emacs-everything-looks-like-a-service.html" rel="alternate"/><published>2026-07-09T23:40:00-07:00</published><updated>2026-07-09T23:40:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-07-09:/devnull/in-emacs-everything-looks-like-a-service.html</id><summary type="html">&lt;p&gt;No, Emacs is not an operating system but this post scrutinizes why some might say that.&lt;/p&gt;</summary><content type="html">&lt;p&gt;A common refrain is that Emacs is an operating system (OS). This isn’t true, but what invites comparison to an OS is its ability to orchestrate applications and utilities above the OS kernel level. The diagram below suggests a truer picture of how Emacs’ relates to an OS and its capabilities.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/ieells/emacs-os-relationship.svg"&gt;&lt;/p&gt;
&lt;p&gt;Emacs’ built-in access to OS system services (file system, network, etc.) coupled with the ability to run other programs makes it routine to improvise client behavior within it. Because of this, Emacs users are able to accomplish many of their computing needs from the different client modes that have been made for it. This gives credence to the notion of “living only in Emacs.”&lt;/p&gt;
&lt;p&gt;In this post, we’ll examine some of the ways Emacs lets you build a client. By the end of this post, you’ll hopefully be convinced that from within Emacs, everything looks like a service.&lt;/p&gt;
&lt;h1&gt;Client-Server Model&lt;/h1&gt;
&lt;p&gt;Let’s first provide some definitions.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://en.wikipedia.org/wiki/Client%E2%80%93server_model"&gt;Client–Server model&lt;/a&gt; is a common computer interaction pattern where a task is partitioned between the provider of a resource (the service) and the requester of that resource (the client). The client issues a &lt;em&gt;request&lt;/em&gt; to the server, and the server in turn returns a &lt;em&gt;response&lt;/em&gt; as shown in the diagram below.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/ieells/client-server.svg"&gt;&lt;/p&gt;
&lt;p&gt;Depending on the implementation, the transaction (request + response) can occur over a network or be local to a system. Client-server models using a network has been most elaborated upon with &lt;a href="https://en.wikipedia.org/wiki/REST"&gt;REST-style software architectures&lt;/a&gt;. Shown in the sequence diagram below is a common implementation pattern for REST-style client server architecture.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/ieells/rest-client-arch.svg"&gt;&lt;/p&gt;
&lt;h1&gt;Emacs as a Client&lt;/h1&gt;
&lt;p&gt;From the diagram above, there are three concerns the client is typically responsible for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UI:&lt;/strong&gt; User interface (if any).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Edge:&lt;/strong&gt; Sub-system concerned with communication with the service. For networked clients, this is the network sub-system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local Database:&lt;/strong&gt; Representation of data that is exchanged or synchronized with the server. How this data is managed is up to the implementation requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the above concerns, Emacs provides numerous libraries both built-in and third-party which can implement a client. Listed below are some built-in libraries with their respective links for further reading:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;UI&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Minibuffers.html"&gt;Minibuffers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Buffers.html"&gt;Buffers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Completion.html"&gt;Completion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Tabulated-List-Mode.html"&gt;Tabulated List Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/vtable/"&gt;Variable Pitch Table (vtable)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/transient/"&gt;Transient&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Client Edge&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/url/"&gt;URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Network.html"&gt;Socket (TCP/UDP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/smtpmail/"&gt;SMTP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Serialization/Deserialization&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-JSON.html"&gt;JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-HTML_002fXML.html"&gt;XML&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Local Database&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collections&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Association-Lists.html"&gt;Association Lists&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Property-Lists.html"&gt;Property Lists&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Hash-Tables.html"&gt;Hash Tables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Database.html"&gt;SQLite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Requirements dictate the amount of complexity required to implement the Emacs client. If there is an existing command line utility that can do the “heavy lifting”, said utility can be reframed as a “service” that can be accessed via a shell call.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/ieells/delgated-rest-client-arch.svg"&gt;&lt;/p&gt;
&lt;h1&gt;Elisp&lt;/h1&gt;
&lt;p&gt;All the libraries mentioned above are accessed through the Emacs Lisp (&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/"&gt;Elisp&lt;/a&gt;) programming language. Elisp is a &lt;a href="https://en.wikipedia.org/wiki/Dynamic_programming_language"&gt;dynamic programming language&lt;/a&gt; which allows for a high degree of improvisation during run-time. This capability allows for complex orchestration of any behavior that is available to Emacs, from Elisp functions to shell commands.&lt;/p&gt;
&lt;h1&gt;Example wttr.in client&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://github.com/chubin/wttr.in"&gt;wttr.in&lt;/a&gt; is a console-oriented weather forecast web-service. It supports &lt;a href="https://github.com/chubin/wttr.in#different-output-formats"&gt;JSON output&lt;/a&gt; so we can build an Emacs &lt;code&gt;wttr&lt;/code&gt; command which will prompt for a location, make the HTTP request, process the JSON response and display the result in the mini-buffer.&lt;/p&gt;
&lt;p&gt;The top-level command &lt;code&gt;wttr&lt;/code&gt; is shown below.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;wttr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Show weather conditions for LOCATION from &lt;/span&gt;&lt;span class="ss"&gt;`https://wttr.in&amp;#39;&lt;/span&gt;&lt;span class="s"&gt; in mini-buffer.&lt;/span&gt;

&lt;span class="s"&gt;Result is also stored in &lt;/span&gt;&lt;span class="ss"&gt;`kill-ring&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interactive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;sWhere (default: local): &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;condition-case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;err&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;wttr--request-url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;jsondb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;fetch-json-as-hash-table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;wttr--report-message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;jsondb&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;kill-new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;ERROR: %s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The wttr.in URL is constructed by the function &lt;code&gt;wttr--request-url&lt;/code&gt; shown below.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;
&lt;span class="normal"&gt;7&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;wttr--request-url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Construct wttr.in URL with LOCATION.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;base-url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-generic-parse-url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;https://wttr.in&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;encoded-location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string-replace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;+&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/%s?0&amp;amp;format=j1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;encoded-location&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;_dummy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-filename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;base-url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-recreate-url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;base-url&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We can subsequently pass that URL into &lt;code&gt;fetch-json-as-hash-table&lt;/code&gt; which does the heavy lifting of retrieving the URL and parsing the JSON response into an Elisp &lt;code&gt;hash-table&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;fetch-json-as-hash-table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Fetch URL with expected JSON response and return a &lt;/span&gt;&lt;span class="ss"&gt;`hash-table&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;data-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-retrieve-synchronously&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;data-buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Failed to fetch data from %s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;unwind-protect&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-current-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;data-buffer&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;;; Move point past the HTTP metadata headers&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;goto-char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;url-http-end-of-headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;;; Parse the remaining JSON buffer into a hash-table&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;json-parse-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;:object-type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;#39;hash-table&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;;; Always kill the downloaded network buffer to prevent memory leaks&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;kill-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;data-buffer&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Finally we can extract the desired values from the JSON response (&lt;code&gt;jsondb&lt;/code&gt;) to populate the message that will sent to the mini-buffer.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;wttr--report-message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;jsondb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Generate weather report message from JSONDB.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;area-buflist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;nearest-area&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;wttr--get-first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;jsondb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;nearest_area&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;area-name&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;map-elt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;wttr--get-first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nearest-area&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;areaName&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;region&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;map-elt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;wttr--get-first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nearest-area&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;region&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;country&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;map-elt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;wttr--get-first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nearest-area&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;country&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;current-condition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;wttr--get-first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;jsondb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;current_condition&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;temp_c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;map-elt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;current-condition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;temp_C&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;temp_f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;map-elt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;current-condition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;temp_F&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;weather-description&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;map-elt&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;wttr--get-first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;current-condition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;weatherDesc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mapc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;lambda&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;string-equal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;area-buflist&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;area-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;country&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%s: %s°C, %s°F %s&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string-join&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;area-buflist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;, &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nv"&gt;temp_c&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nv"&gt;temp_f&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nv"&gt;weather-description&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/kickingvegas/40290cef2751c133666eb19921f91eee"&gt;wttr.el source&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Closing Thoughts&lt;/h1&gt;
&lt;p&gt;At this point, hopefully you are convinced of the title assertion that from Emacs, everything looks like a service. Furthermore, many of the APIs offered by Emacs work at a high-level of abstraction. Consider that the lines of code for &lt;code&gt;wttr.el&lt;/code&gt; weighs in at 67. (Result using the &lt;code&gt;cloc&lt;/code&gt; utility.)&lt;/p&gt;
&lt;p&gt;If that’s too much, then imagine an alternate implementation where the actual network request and JSON processing is done in a Python script called &lt;code&gt;weather&lt;/code&gt;. Then the Elisp command to invoke it is just the code shown below.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;
&lt;span class="normal"&gt;7&lt;/span&gt;
&lt;span class="normal"&gt;8&lt;/span&gt;
&lt;span class="normal"&gt;9&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;weather&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Call weather script with LOCATION and show result in minibuffer.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interactive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;sWhere (default: local): &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;weather-cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;weather&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%s %s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;weather-cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;weather-cmd&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;shell-command-to-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;kill-new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the above implementation, the shell command becomes effectively the “service” to make a request to.&lt;/p&gt;
&lt;p&gt;As Elisp is a dynamic programming language, it can allow for integration of Elisp libraries with command line utilities in an improvised fashion.&lt;/p&gt;
&lt;p&gt;This capability is compelling to users who recognize the opportunities it can offer.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/></entry><entry><title>Scheduling Future Tasks in Emacs</title><link href="http://yummymelon.com/devnull/scheduling-future-tasks-in-emacs.html" rel="alternate"/><published>2026-06-24T12:25:00-07:00</published><updated>2026-06-24T12:25:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-06-24:/devnull/scheduling-future-tasks-in-emacs.html</id><summary type="html">&lt;p&gt;If you miss using &lt;code&gt;at&lt;/code&gt;, you can use Emacs &lt;code&gt;run-at-time&lt;/code&gt; as a substitute, with caveats. Also described is a way to use &lt;code&gt;run-at-time&lt;/code&gt; to post a macOS notification at a future date.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Back when I had to run a lot of long-running simulations on Unix systems, I became an ardent user of the &lt;a href="https://en.wikipedia.org/wiki/At_(command)"&gt;at&lt;/a&gt; command, which lets one schedule a program to run at a future date. I also used &lt;code&gt;at&lt;/code&gt; to schedule different notifications, like sending a message to a pager (yer, how quaint).&lt;/p&gt;
&lt;p&gt;Moving to macOS (nee OS X) changed that, as it disabled by default the &lt;code&gt;at&lt;/code&gt; command due to battery life management. As I was unwilling to give up battery life for &lt;code&gt;at&lt;/code&gt;, I learned to live without it. That was, until I learned that Emacs had &lt;code&gt;run-at-time&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;run-at-time&lt;/code&gt; command does as it says on the tin: it will run a &lt;em&gt;function&lt;/em&gt; at some future specification of &lt;em&gt;time&lt;/em&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;run-at-time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;REPEAT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;FUNCTION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;rest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That specification of time is “natural language-like” in that multiple string representations of time can be accepted, both absolute and relative.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;function&lt;/em&gt; can be anything Elisp offers, but &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell.html"&gt;running shell commands&lt;/a&gt; is what makes &lt;code&gt;run-at-time&lt;/code&gt; a substitute for &lt;code&gt;at&lt;/code&gt;, with some caveats.&lt;/p&gt;
&lt;p&gt;Jobs that are submitted by &lt;code&gt;run-at-time&lt;/code&gt; can be listed out via the &lt;code&gt;list-timers&lt;/code&gt; command. You can cancel a job (actually a timer object) using the &lt;code&gt;c&lt;/code&gt; key, bound to &lt;code&gt;timer-list-cancel&lt;/code&gt;. Note that Emacs disables &lt;code&gt;list-timers&lt;/code&gt; by default as it exposes timers that you should likely not mess with. Be forewarned.&lt;/p&gt;
&lt;p&gt;Another caveat is that using &lt;code&gt;run-at-time&lt;/code&gt; presumes that your Emacs session is still running at that future time. If you have Emacs running all the time (particularly as a &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html"&gt;server&lt;/a&gt;) then this should work without complication.&lt;/p&gt;
&lt;h1&gt;Example: Scheduling a macOS Notification&lt;/h1&gt;
&lt;p&gt;A common use I have for &lt;code&gt;run-at-time&lt;/code&gt; is to send myself a notification at a future time. The way I prefer to be notified is using the &lt;a href="https://support.apple.com/guide/mac-help/get-notifications-mchl2fb1258f/mac"&gt;macOS Notification Center&lt;/a&gt;. Here’s an example of what a notification looks like:&lt;/p&gt;
&lt;p align='center'&gt;
&lt;img src='images/scheduling-future-tasks-emacs/nota-notification.png'  /&gt;
&lt;/p&gt;

&lt;p&gt;The above notification was sent using a &lt;a href="https://support.apple.com/guide/shortcuts-mac/intro-to-shortcuts-apdf22b0444c/mac"&gt;macOS Shortcut&lt;/a&gt; I’ve defined named &lt;em&gt;nota&lt;/em&gt;. Shown below is how &lt;em&gt;nota&lt;/em&gt; is defined in the Shortcuts app.&lt;/p&gt;
&lt;p align='center'&gt;
&lt;img src='images/scheduling-future-tasks-emacs/nota-screenshot.png'  /&gt;
&lt;/p&gt;

&lt;p&gt;A macOS shortcut can be invoked from the command line. We can wrap this capability in the Elisp function &lt;code&gt;cc/run-nota&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;
&lt;span class="normal"&gt;7&lt;/span&gt;
&lt;span class="normal"&gt;8&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cc/run-nota&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Run nota shortcut with MSG.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;process-lines&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;shortcuts&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;run&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;nota&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;-i&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Finally we can schedule &lt;code&gt;cc/run-nota&lt;/code&gt; with &lt;code&gt;run-at-time&lt;/code&gt; through the wrapper command &lt;code&gt;cc/notify-at&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cc/notify-at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;optional&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;start-time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Send a nota notification with MSG at START-TIME.&lt;/span&gt;

&lt;span class="s"&gt;This command invokes &lt;/span&gt;&lt;span class="ss"&gt;`cc/run-nota&amp;#39;&lt;/span&gt;&lt;span class="s"&gt; with MSG at START-TIME passed into&lt;/span&gt;
&lt;span class="ss"&gt;`run-at-time&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interactive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;start-time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;start-time&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="nv"&gt;start-time&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Time: &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Message: &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;run-at-time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;start-time&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="nf"&gt;#&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;cc/run-nota&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Show notification “%s” in/at %s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;start-time&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Users wishing to install the &lt;em&gt;nota&lt;/em&gt; macOS shortcut can download it via the following iCloud link:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.icloud.com/shortcuts/8e180b939daa4ab5a7f03e326577fa30"&gt;nota shortcut (via iCloud)&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Closing Thoughts&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;run-at-time&lt;/code&gt; is an &lt;a href="https://rossabaker.com/blog/underappreciated-emacs-built-ins/"&gt;underappreciated built-in&lt;/a&gt; which has given me much of what I used to use &lt;code&gt;at&lt;/code&gt; for. If you use Emacs for implementing any kind of automation, you should include &lt;code&gt;run-at-time&lt;/code&gt; as part of your toolbox for it.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="macos"/></entry><entry><title>Revisiting Emacs Keyboard Macros with a Mouse</title><link href="http://yummymelon.com/devnull/revisiting-emacs-keyboard-macros-with-a-mouse.html" rel="alternate"/><published>2026-06-04T09:35:00-07:00</published><updated>2026-06-04T09:35:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-06-04:/devnull/revisiting-emacs-keyboard-macros-with-a-mouse.html</id><summary type="html">&lt;p&gt;Yes, recording an Emacs keyboard macro with a mouse is a thing.&lt;/p&gt;</summary><content type="html">&lt;p&gt;“No-code automation” has been in Emacs far longer than its usage as a term of art. Better known as &lt;em&gt;keyboard macros&lt;/em&gt;, this feature has allowed Emacs users to achieve their bespoke needs for decades, all without needing to know a whit about Lisp. I can testify to this personally: though I’ve been using Emacs since the early 90’s, only in the past three years has writing Elisp played any part in it. For most of my Emacs journey, I got by with just making keyboard macros.&lt;/p&gt;
&lt;p&gt;If you’re unfamiliar with them, here are two links you should get to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Keyboard-Macros.html"&gt;The official Emacs manual for keyboard macros&lt;/a&gt; ((emacs) Keyboard Macros)&lt;/li&gt;
&lt;li&gt;Mickey Petersen’s post - &lt;a href="https://www.masteringemacs.org/article/keyboard-macros-are-misunderstood"&gt;“Keyboard Macros are Misunderstood”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My addition to the knowledge of those posts is to boost this fact: &lt;strong&gt;mouse events can also be captured by a keyboard macro&lt;/strong&gt;. If you know where a command &lt;em&gt;is&lt;/em&gt; in a menu, you can also record clicking on it. I think the ability to combine both keyboard and mouse events in a keyboard macro is compelling, so much so that I decided to emphasize this in Anju.&lt;/p&gt;
&lt;p&gt;The recent &lt;a href="https://github.com/kickingvegas/anju"&gt;Anju&lt;/a&gt; v1.5.0 release added the menu “Tools › Macro Recorder” to the menu bar as shown below.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/anju-kmacros/anju-main-menu-tools-kmacro.png"&gt;&lt;/p&gt;
&lt;p&gt;With this, mouse enthusiasts can create and run keyboard macros without need to recall keybindings or deal with inconsistent command naming, as observed in Petersen’s post.&lt;/p&gt;
&lt;p&gt;One thing that I TILed about keyboard macros in building this menu was the command &lt;code&gt;list-keyboard-macros&lt;/code&gt; (see &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Kmacro-Menu.html"&gt;(emacs) Kmacro menu&lt;/a&gt;). This command is available in the “Macro Recorder” menu as the item “List macros”. This lets you manage multiple keyboard macros that have been defined during a session.&lt;/p&gt;
&lt;p&gt;If this interests you, I invite you install &lt;a href="https://melpa.org/#/anju"&gt;Anju from MELPA&lt;/a&gt; and give it a try. Let me know &lt;a href="https://github.com/kickingvegas/anju/discussions/121"&gt;what you think&lt;/a&gt;!&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="anju"/></entry><entry><title>Opening macOS Finder Folders in Emacs with Scrim</title><link href="http://yummymelon.com/devnull/opening-macos-finder-folders-in-emacs-with-scrim.html" rel="alternate"/><published>2026-06-01T19:00:00-07:00</published><updated>2026-06-01T19:00:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-06-01:/devnull/opening-macos-finder-folders-in-emacs-with-scrim.html</id><summary type="html">&lt;p&gt;Finder fun with the Scrim custom URL scheme and some musings on software orchestration.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="img" src="images/open-in-emacs-shortcut/open-in-emacs-quick-action.png"&gt;&lt;/p&gt;
&lt;h1&gt;TL;DR&lt;/h1&gt;
&lt;p&gt;If you use both &lt;a href="http://yummymelon.com/scrim/"&gt;Scrim&lt;/a&gt; and Emacs on macOS, you can “Open in Emacs” a file or directory from Finder via the “Quick Actions” context menu. This is implemented as a macOS Shortcut, shared as an iCloud link:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.icloud.com/shortcuts/f5c8499183dc44ec89cebfc78bfb0433"&gt;“Open in Emacs” Shortcut iCloud Link&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Motivation&lt;/h1&gt;
&lt;p&gt;I find macOS &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Drag-and-Drop.html"&gt;drag-and-drop&lt;/a&gt; operations from Finder to Emacs (in particular, the NS variant) to be frustratingly inconsistent.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dragging a file or folder from Finder to a non-Dired buffer will visit it.&lt;ul&gt;
&lt;li&gt;Not always! This behavior can be mode-specific.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dragging a file or folder from Finder to a Dired buffer will copy it.&lt;/li&gt;
&lt;li&gt;Dragging a file or folder from Finder to the Emacs icon in the Dock will visit it in a new frame.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add to that, drag-and-drop isn’t graceful with a long traversal path on a large screen.&lt;/p&gt;
&lt;p&gt;Ultimately, what I want is a single consistent action to “Open in Emacs” a file or directory from Finder. This post describes a solution for achieving this using the &lt;a href="http://yummymelon.com/scrim/"&gt;Scrim&lt;/a&gt; and &lt;a href="https://support.apple.com/guide/shortcuts-mac/intro-to-shortcuts-apdf22b0444c/mac"&gt;macOS Shortcuts&lt;/a&gt; apps.&lt;/p&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;h2&gt;Scrim&lt;/h2&gt;
&lt;p&gt;Scrim is an Org protocol proxy app that I had &lt;a href="http://yummymelon.com/devnull/announcing-scrim--an-org-protocol-proxy-for-emacs-on-macos.html"&gt;published on the App Store&lt;/a&gt; last year. Implementing Scrim was largely an exercise in reverse-engineering &lt;code&gt;emacsclient&lt;/code&gt; to be a macOS app. This is because macOS apps these days (macOS 10.14+) are packaged to execute as a &lt;a href="https://developer.apple.com/documentation/security/hardened-runtime"&gt;hardened runtime&lt;/a&gt; (aka “sandboxed”). Among the many things a hardened runtime restricts is the ability for an app to &lt;a href="https://en.wikipedia.org/wiki/Exec_(system_call)"&gt;exec&lt;/a&gt; another program outside of its sandbox (like &lt;code&gt;emacsclient&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;In lieu of that restriction, macOS provides different APIs to let apps work with other apps, among them &lt;em&gt;Services&lt;/em&gt; (more below), &lt;a href="https://developer.apple.com/documentation/intents/"&gt;Intents&lt;/a&gt; (a whole different topic worthy of another post), and &lt;a href="https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app"&gt;custom URL schemes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A custom URL scheme is what Scrim provides to allow other apps to do &lt;code&gt;emacsclient&lt;/code&gt;-like things like opening a file. By design, Scrim does not surface all the capabilities of &lt;code&gt;emacsclient&lt;/code&gt; such as arbitrary Elisp execution. The custom URL scheme itself is &lt;code&gt;scrim://&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Currently the &lt;code&gt;scrim://&lt;/code&gt; URL scheme supports the following two actions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Visit a file or directory (e.g. &lt;code&gt;scrim://open?file=~/.profile&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Open an Info node (e.g. &lt;code&gt;scrim://info?node=(eshell) Top&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For this post, we will concern ourselves with (1) visiting (opening) a file or directory.&lt;/p&gt;
&lt;p&gt;Note that on a system where a custom URL scheme is installed, &lt;em&gt;any&lt;/em&gt; app on that system can open said custom URL scheme.&lt;/p&gt;
&lt;h2&gt;Services&lt;/h2&gt;
&lt;p&gt;The macOS &lt;a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/SysServices/introduction.html#/apple_ref/doc/uid/10000101-SW1"&gt;Services API&lt;/a&gt; allows an application to access the functionality of another.&lt;/p&gt;
&lt;p&gt;Apple uses this API to provide tools to help orchestrate workflows between different applications, namely AppleScript, Automator, and Shortcuts. Note that as of this writing, AppleScript and Automator are in maintenance mode, and Shortcuts is intended to supersede Automator. Once a workflow is defined, it is usually accessed via a context menu sub-menu labeled “Quick Actions” or “Services”.&lt;/p&gt;
&lt;h1&gt;“Open in Emacs” Shortcut&lt;/h1&gt;
&lt;p&gt;With the above background out of the way, let's turn our attention to the Shortcut itself.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/open-in-emacs-shortcut/open-in-emacs-shortcut.png"&gt;&lt;/p&gt;
&lt;p&gt;The shortcut invokes a Python script which forms the &lt;code&gt;scrim://open?file&lt;/code&gt; custom URL and then opens it with the &lt;code&gt;open&lt;/code&gt; command line utility.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;
&lt;span class="normal"&gt;7&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;

&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;scrim://open?file=&lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="n"&gt;cmdList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;open&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmdList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This shortcut can be installed via iCloud link: &lt;a href="https://www.icloud.com/shortcuts/f5c8499183dc44ec89cebfc78bfb0433"&gt;“Open in Emacs” Shortcut iCloud Link&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When installed, the menu item "Open in Emacs" will appear in the "Quick Actions" sub-menu when the context menu is raised on an Finder item.&lt;/p&gt;
&lt;h1&gt;“scrim link” Shortcut&lt;/h1&gt;
&lt;p&gt;&lt;img alt="img" src="images/open-in-emacs-shortcut/scrim-link-shortcut.png"&gt;&lt;/p&gt;
&lt;p&gt;What if you want just the Scrim URL to open a file or directory instead? The shortcut "scrim link" shown above provides that, putting the URL into the system clipboard to paste into another native macOS app like Notes or TextEdit. Such apps that recognize custom URL schemes will treat the URL as a “live” link.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/open-in-emacs-shortcut/scrim-notepad-example.png"&gt;&lt;/p&gt;
&lt;p&gt;This shortcut is available at &lt;a href="https://www.icloud.com/shortcuts/195d64f6e4474826b71834fd06523c14"&gt;“scrim link” Shortcut iCloud Link&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Closing Thoughts&lt;/h1&gt;
&lt;p&gt;While the behavior of "Open in Emacs" seems modest, a &lt;em&gt;lot&lt;/em&gt; of things need to happen under the hood to orchestrate the different apps required to make it happen on macOS. But taking a step back, I'd observe that this is not unique to just macOS alone. Other contemporary operating systems are following suit as seen with &lt;a href="https://en.wikipedia.org/wiki/Snap_(software)"&gt;Snap (Ubuntu)&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Flatpak"&gt;Flatpak&lt;/a&gt; for Linux, and UWP for Windows. Such isolating practices by OS vendors are &lt;em&gt;not&lt;/em&gt; going away.&lt;/p&gt;
&lt;p&gt;Running counter to this is the spate of interest in orchestrating apps and/or services together using AI tools, whose approach so far is to bypass all hardened runtime boundaries.&lt;/p&gt;
&lt;p&gt;Then there is comparison to the Emacs ecosystem, where orchestration of apps (or rather modes) is routinely done via Elisp. By implementation circumstance there are effectively no concerns for security boundaries within the internals of Emacs.&lt;/p&gt;
&lt;p&gt;There's a lot of ground to ruminate on, in particular software malleability and orchestration, which I intend to do with future posts on this blog.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="macos"/><category term="scrim"/><category term="software"/></entry><entry><title>Anju v1.5.0 Update</title><link href="http://yummymelon.com/devnull/anju-v150-update.html" rel="alternate"/><published>2026-05-28T10:00:00-07:00</published><updated>2026-05-28T10:00:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-05-28:/devnull/anju-v150-update.html</id><summary type="html">&lt;p&gt;Anju v1.5.0 update out in the wild. Lots of goodies in it.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Heads up for readers of this blog who are using &lt;a href="https://github.com/kickingvegas/anju"&gt;Anju&lt;/a&gt; or are mouse-curious Emacs users. I’ve recently released the &lt;a href="https://github.com/kickingvegas/anju/discussions/121"&gt;v1.5.0 update&lt;/a&gt; for it, now available on &lt;a href="https://melpa.org/#/anju"&gt;MELPA&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Many new features and enhancements are in this update as outlined below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Context Menus&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Selected-Text-Commands.html"&gt;Enhancements to selected text&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;Query Replace (Regexp)…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Enhancements to Org mode support&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Org-Headline-Context-Menu.html"&gt;Headline&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;Change to Body&lt;/li&gt;
&lt;li&gt;Sort…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Org-Item-Context-Menu.html"&gt;Item&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;Cycle Bullet…&lt;/li&gt;
&lt;li&gt;Sort…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Org-Table-Context-Menu.html"&gt;Table&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;Sort…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Org-_201cCopy-as_2026_201d-Context-Menu.html"&gt;Copy as…&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Org-Insert-Link.html"&gt;Link…&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Added support for &lt;a href="https://kickingvegas.github.io/anju/Makefile-Context-Menu.html"&gt;Makefile mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Added support for &lt;a href="https://kickingvegas.github.io/anju/Info-Context-Menu.html"&gt;Info mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Added support for &lt;a href="https://kickingvegas.github.io/anju/Compilation_002fGrep-Context-Menu.html"&gt;Compilation/Grep mode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Main Menu&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tools menu enhancements&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Tools-Menu-Features.html"&gt;Global Org Commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Macro-Recorder-Menu.html"&gt;Macro Recorder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Edit › Delete menu enhancement&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kickingvegas.github.io/anju/Edit-Menu-Features.html"&gt;Delete Duplicate Lines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Users who work with both Org mode and Markdown formatted text should find interest in the &lt;a href="https://kickingvegas.github.io/anju/Org-_201cCopy-as_2026_201d-Context-Menu.html"&gt;Copy as…&lt;/a&gt; capability added to the Org mode context menu. This feature lets one use Org mode as the primary means of authoring text, exporting to Markdown (or another format) using a right-click mouse copy action.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/anju-1-5/anju-context-menu-org-copy-as.png"&gt;&lt;/p&gt;
&lt;p&gt;Thanks to Marcin Borkowski whose post &lt;a href="https://mbork.pl/2021-05-02_Org-mode_to_Markdown_via_the_clipboard"&gt;“Org-mode to Markdown via the clipboard”&lt;/a&gt;, inspired this feature.&lt;/p&gt;
&lt;p&gt;Anju also provides a complementary command “Paste Markdown as Org” for Org mode which lets one paste copied Markdown text into an Org file, doing the conversion of Markdown to Org under the hood using &lt;code&gt;pandoc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/anju-1-5/anju-paste-markdown-as-org.png"&gt;&lt;/p&gt;
&lt;p&gt;In summary, this is a big release with many changes. I invite you to explore all the links above to see what’s available in Anju.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="org mode"/><category term="anju"/></entry><entry><title>Using the Mouse for Emacs Rectangle Commands</title><link href="http://yummymelon.com/devnull/using-the-mouse-for-emacs-rectangle-commands.html" rel="alternate"/><published>2026-05-18T09:45:00-07:00</published><updated>2026-05-18T09:45:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-05-18:/devnull/using-the-mouse-for-emacs-rectangle-commands.html</id><summary type="html">&lt;p&gt;Never look up Emacs rectangle commands in the manual again. Announcing support for rectangle commands in Anju v1.4.0 update.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="img" src="images/rectangle-mouse-commands/anju-main-menu-edit-rectangle.png"&gt;&lt;/p&gt;
&lt;p&gt;Of all the built-in editing commands in Emacs, the commands that work with &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Rectangles.html"&gt;rectangles&lt;/a&gt; delight me the most. Once understood, they can save effort in many situations.&lt;/p&gt;
&lt;p&gt;That said, the biggest downside to rectangles is the amount of setup it takes to &lt;em&gt;use&lt;/em&gt; them. As with many things Emacs, by default you have to memorize a bunch of keybindings to get anything done with them.&lt;/p&gt;
&lt;p&gt;In Casual, I addressed the above downside by providing a &lt;a href="https://kickingvegas.github.io/casual/Edit-commands.html#Rectangle_203a-_0028casual_002deditkit_002drectangle_002dtmenu_0029"&gt;Transient menu for rectangle commands&lt;/a&gt;. Since building that, I've come to use rectangle commands routinely. But even then, there is ceremony to set up a rectangle selection.&lt;/p&gt;
&lt;p&gt;With my recent work on mouse-driven interactions in Anju, I’ve learned that &lt;a href="http://yummymelon.com/devnull/some-nice-to-know-keybindings-when-using-the-mouse-in-emacs.html"&gt;rectangle selection&lt;/a&gt; is trivial via &lt;code&gt;C-M-&amp;lt;mouse-1&amp;gt;&lt;/code&gt; dragging. Once selected, having a menu of rectangle commands makes working with them even easier. So I made one for the latest &lt;a href="https://github.com/kickingvegas/anju/discussions/94"&gt;v1.4.0 update for Anju&lt;/a&gt;, now on &lt;a href="https://melpa.org/#/anju"&gt;MELPA&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The “Rectangle” sub-menu is available via the &lt;a href="https://kickingvegas.github.io/anju/Edit-Menu-Features.html"&gt;main menu bar “Edit” menu&lt;/a&gt; (as shown in the screenshot above) or via &lt;a href="https://kickingvegas.github.io/anju/Rectangle-Context-Menu.html"&gt;context menu&lt;/a&gt;. Using rectangle commands in conjunction with the &lt;code&gt;align-regexp&lt;/code&gt; (“Edit › Align Regexp…”) and &lt;code&gt;whitespace-cleanup&lt;/code&gt; (“Edit › Delete › Whitespace Cleanup”) can make short work of editing text that is laid out in columns. Anju makes both of these commands available from the main menu.&lt;/p&gt;
&lt;p&gt;Side note: on using &lt;code&gt;C-M-&amp;lt;mouse-1&amp;gt;&lt;/code&gt;, sometimes Emacs will only read &lt;code&gt;M-&amp;lt;mouse-1&amp;gt;&lt;/code&gt; and do a &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Secondary-Selection.html"&gt;secondary selection&lt;/a&gt;, leaving an unwanted highlight. Enter &lt;code&gt;M-&amp;lt;mouse-1&amp;gt;&lt;/code&gt; to dismiss the highlight.&lt;/p&gt;
&lt;p&gt;If you find Anju to be useful, please support its development by &lt;a href="https://buymeacoffee.com/kickingvegas"&gt;buying me a coffee&lt;/a&gt;. I’ve got a &lt;a href="https://github.com/kickingvegas/anju/issues"&gt;number of new features planned for it&lt;/a&gt;.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="anju"/></entry><entry><title>Enhancing Elisp Development with Context Menus</title><link href="http://yummymelon.com/devnull/enhancing-elisp-development-with-context-menus.html" rel="alternate"/><published>2026-05-11T11:00:00-07:00</published><updated>2026-05-11T11:00:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-05-11:/devnull/enhancing-elisp-development-with-context-menus.html</id><summary type="html">&lt;p&gt;Announcing support for Elisp development in the Anju v1.3.0 update.&lt;/p&gt;</summary><content type="html">&lt;p&gt;As celebrated as Emacs is for its programmability, I find the actual process of developing Emacs Lisp (Elisp) to be quite underwhelming. Developing in Elisp has meant learning its libraries and idioms, which is to be expected. What I really don’t care for though are the arcane keybindings associated with doing basic things like evaluating, navigating (Xref), and debugging (especially debugging). While I’ve already committed to muscle memory most of said keybindings, I’d argue that having a mouse-based “point and click” interface is beneficial for both novice and experienced Elisp developers alike, as it lets one focus on the code and not on recalling the right key incantation &lt;em&gt;all the time&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The addition of &lt;code&gt;context-menu-mode&lt;/code&gt; in Emacs 28 has provided the opportunity to build a mouse-based UI for Elisp development and I’m happy to announce the availability of one in the latest &lt;a href="https://github.com/kickingvegas/anju/discussions/84"&gt;v1.3.0 update to Anju&lt;/a&gt;, now on &lt;a href="https://melpa.org/#/anju"&gt;MELPA&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To get an idea of what’s available in the v1.3.0 update, here’s a screenshot of the context menu when normally editing an Elisp file where the point is on the symbol &lt;code&gt;foo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/elisp-context-menus/anju-context-menu-elisp.png"&gt;&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;foo&lt;/code&gt; is instrumented for debugging and run, the context menu is adjusted to provide Edebug commands as shown below.&lt;/p&gt;
&lt;p&gt;&lt;img alt="img" src="images/elisp-context-menus/anju-context-menu-edebug.png"&gt;&lt;/p&gt;
&lt;p&gt;The context menu enhancements provided by Anju take full advantage of built-in functions that identify context at &lt;em&gt;point&lt;/em&gt;. Is the point on a symbol? In a function? In an ERT test? On a &lt;code&gt;lambda&lt;/code&gt;? The menu items added by Anju take these factors into account to provide a &lt;em&gt;relevant&lt;/em&gt; menu offering.&lt;/p&gt;
&lt;p&gt;Users can read more about Anju’s context menu enhancements for Elisp development at &lt;a href="https://kickingvegas.github.io/anju/Emacs-Lisp-Context-Menu.html"&gt;Emacs Lisp Context Menu (Anju User Guide)&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Some backstory with Edebug&lt;/h1&gt;
&lt;p&gt;Back in 2024, I did a deep dive into Edebug so as to &lt;a href="http://yummymelon.com/presentations/demystifying-edebug/demystifying-edebug.html"&gt;give a presentation on it&lt;/a&gt; at the &lt;a href="https://www.meetup.com/emacs-sf/events/301454544/?eventOrigin=group_events_list"&gt;EmacsSF meetup&lt;/a&gt;. In doing so, I realized that all the core features for having an Elisp IDE were there, but no good UI to present it. I set upon prototyping one using Transient, with decidedly mixed results due to both Edebug and Transient fighting each other over window management.&lt;/p&gt;
&lt;p&gt;A later &lt;a href="http://yummymelon.com/devnull/prototyping-a-toolbar-ui-for-edebug.html"&gt;Edebug UI prototype using the toolbar&lt;/a&gt; was attempted but it also had its issues, particularly with layout (or lack thereof) and toolbar-specific bugs in macOS. But a benefit of using the toolbar was that its UI interactions did not interfere with window management. The context menu also shares this benefit.&lt;/p&gt;
&lt;h1&gt;Closing Thoughts&lt;/h1&gt;
&lt;p&gt;The context menu has markedly improved my developer experience with Elisp, as I find myself using it more than typing out keybindings. Even with keybindings, I’ll use the &lt;a href="https://kickingvegas.github.io/casual/Elisp.html"&gt;Casual Elisp Transient&lt;/a&gt; to accomplish most Elisp interactions that don’t require Edebug.&lt;/p&gt;
&lt;p&gt;That said, I’m not here to eschew keybindings. I still use them, when convenient. But in many circumstances they are not. The context menu can provide an easier way to achieve the same thing. Isn’t that a good thing?&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="anju"/></entry><entry><title>Bulk Search &amp; Replace Commands for Files and Buffers in Emacs</title><link href="http://yummymelon.com/devnull/bulk-search-replace-commands-for-files-and-buffers-in-emacs.html" rel="alternate"/><published>2026-04-29T22:15:00-07:00</published><updated>2026-04-29T22:15:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-04-29:/devnull/bulk-search-replace-commands-for-files-and-buffers-in-emacs.html</id><summary type="html">&lt;p&gt;An inventory of bulk commands to search and/or replace multiple files or buffers in Emacs.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Emacs offers a dizzying number of commands to perform bulk operations on multiple files or buffers. These commands are quite capable and can make trivial the execution of workflows that would be heroic in other editors. Unfortunately these commands are also difficult to discover as a number of them are not featured in easily accessible menus.&lt;/p&gt;
&lt;p&gt;This post calls attention to bulk commands related to search and replace in Emacs and how their discoverability can be improved using the &lt;a href="https://kickingvegas.github.io/casual/index.html"&gt;Casual&lt;/a&gt; and &lt;a href="https://kickingvegas.github.io/anju/"&gt;Anju&lt;/a&gt; UI packages.&lt;/p&gt;
&lt;h1&gt;Regular Expressions&lt;/h1&gt;
&lt;p&gt;Before moving forward, let’s talk about how Emacs handles regular expression syntax in search and/or replace commands. Whenever using an Emacs command involving a regular expression (regexp), you will need to understand what kind of regexp syntax is expected. There are largely two to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Syntax-of-Regexps.html"&gt;Emacs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;grep (implementation dependent)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Commands using grep-style regexps do so because they invoke the local install of the &lt;code&gt;grep&lt;/code&gt; utility. The grep regex syntax is implementation dependent. If you use GNU grep, you can read its &lt;a href="https://www.gnu.org/software/grep/manual/html_node/Regular-Expressions.html"&gt;syntax here&lt;/a&gt;, which share some &lt;a href="https://www.gnu.org/software/grep/manual/html_node/Character-Classes-and-Bracket-Expressions.html"&gt;character classes&lt;/a&gt; with &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Char-Classes.html"&gt;Emacs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://kickingvegas.github.io/casual/RE_002dBuilder-Usage.html"&gt;Casual RE-Builder&lt;/a&gt; package is a great way to work with Emacs regexps as it provides a command to escape the regexp properly for interactive input. &lt;code&gt;re-builder&lt;/code&gt; alone only provides a regexp that can be used in Elisp code.&lt;/p&gt;
&lt;h1&gt;Bulk Search &amp;amp; Replace Commands&lt;/h1&gt;
&lt;p&gt;The table below shows the different search and/or replace workflows and their associated commands.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left;"&gt;Workflows&lt;/th&gt;
&lt;th style="text-align: left;"&gt;Command&lt;/th&gt;
&lt;th style="text-align: left;"&gt;Regexp Type&lt;/th&gt;
&lt;th style="text-align: left;"&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search regexp in files in directory&lt;/td&gt;
&lt;td style="text-align: left;"&gt;rgrep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;grep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;AKA “Find in Files.” Results can be edited using writeable grep (wgrep).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search regexp in files in project&lt;/td&gt;
&lt;td style="text-align: left;"&gt;project-find-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;grep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search regexp in files in directory and present in Dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;find-grep-dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;grep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search regexp in tagged files&lt;/td&gt;
&lt;td style="text-align: left;"&gt;tags-search&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search file names matching shell pattern in directory and present in Dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;find-name-dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;shell&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search file names matching regexp in directory and present in Dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;find-lisp-find-dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Implementation uses Emacs regexp type.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search file names matching regexp in current directory and present in Dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;casual-dired-find-dired-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Wrapper around find-lisp-find-dired using current directory. Available via Casual.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Query replace regexp in project files&lt;/td&gt;
&lt;td style="text-align: left;"&gt;project-query-replace-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Incremental search marked files with plain text&lt;/td&gt;
&lt;td style="text-align: left;"&gt;dired-do-isearch&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in Dired.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Incremental search marked files with regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;dired-do-isearch-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in Dired.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Query replace regexp in marked files&lt;/td&gt;
&lt;td style="text-align: left;"&gt;dired-do-query-replace-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in Dired.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search regexp in marked files&lt;/td&gt;
&lt;td style="text-align: left;"&gt;dired-do-find-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;grep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in Dired.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Search regexp in marked files and display first match&lt;/td&gt;
&lt;td style="text-align: left;"&gt;dired-do-search&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in Dired.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Replace regexp in marked files&lt;/td&gt;
&lt;td style="text-align: left;"&gt;dired-do-find-regexp-and-replace&lt;/td&gt;
&lt;td style="text-align: left;"&gt;grep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in Dired.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Replace regexp in marked files using diff interface.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;dired-do-replace-regexp-as-diff&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in Dired.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Incremental search marked buffers with plain text&lt;/td&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-isearch&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in IBuffer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Incremental search marked buffers with plain text with regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-isearch-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in IBuffer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;View lines which match regexp in marked buffers&lt;/td&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-occur&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in IBuffer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Query replace plain text in marked buffers&lt;/td&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-query-replace&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in IBuffer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Query replace regexp in marked buffers&lt;/td&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-query-replace-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;For use in IBuffer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Run git grep, searching for regexp in directory.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;vc-git-grep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;grep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Replace all references to identifier.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;xref-find-references-and-replace&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Interactively replace identifier in current xref buffer.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;xref-query-replace-in-results&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Rename symbol at point for project.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;eglot-rename&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Requires Eglot support.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Replace regexp in files using diff interface.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;multi-file-replace-regexp-as-diff&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Start multi-buffer incremental search on a list of files.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;multi-isearch-files&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Start multi-buffer incremental regexp search on a list of files.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;multi-isearch-files-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Start multi-buffer incremental search on a list of buffers.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;multi-isearch-buffers&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Start multi-buffer incremental regexp search on a list of buffers.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;multi-isearch-buffers-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Show all lines in buffers containing a match for regexp.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;multi-occur&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Show all lines containing a match for regexp in buffers that match bufregexp.&lt;/td&gt;
&lt;td style="text-align: left;"&gt;multi-occur-in-matching-buffers&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Emacs&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Choice of command depends on the quantity and specificity of files or buffers to work on. Both Dired and IBuffer marking allow for specific selection of files or buffers respectively.&lt;/p&gt;
&lt;p&gt;Many of the commands in the table above are discoverable via menu using the Casual and Anju packages. Hierarchical menu categorization aids in the discovery and recognition of these commands, making them more &lt;strong&gt;usable&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left;"&gt;Command&lt;/th&gt;
&lt;th style="text-align: left;"&gt;UI&lt;/th&gt;
&lt;th style="text-align: left;"&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;rgrep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-editkit-main-tmenu › (/) Search/Replace › (g) Find in Files…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Search-_0026-Replace-commands.html"&gt;Casual EditKit&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;rgrep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(Menu-bar) Edit › Search › Search in Files…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/anju/Edit-Menu-Features.html"&gt;Anju.&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;project-find-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(Menu-bar) Edit › Search › Search in Project Files…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;tags-search&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(Menu-bar) Edit › Search › Search Tagged Files…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;project-query-replace-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(Menu-bar) Edit › Replace › Replace in Project Files…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;find-name-dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-editkit-main-tmenu › (/) Search/Replace › (d) Files…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Search-_0026-Replace-commands.html"&gt;Casual EditKit&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;find-grep-dired&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-editkit-main-tmenu › (/) Search/Replace › (G) Files containing text…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Search-_0026-Replace-commands.html"&gt;Casual EditKit&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;casual-dired-find-dired-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-dired-tmenu › (f) Filter by name…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Dired-Usage.html"&gt;Casual Dired&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;dired-do-isearch&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-dired-tmenu › (/) Search/Replace › (C-s) I-search…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Dired-Search-_0026-Replace-_0028Search-_0026-Replace_203a_0029.html"&gt;Casual Dired&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;dired-do-isearch-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-dired-tmenu › (/) Search/Replace › (M-s) I-search regexp…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Dired-Search-_0026-Replace-_0028Search-_0026-Replace_203a_0029.html"&gt;Casual Dired&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;dired-do-query-replace-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-dired-tmenu › (/) Search/Replace › (r) Query regexp and replace…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Dired-Search-_0026-Replace-_0028Search-_0026-Replace_203a_0029.html"&gt;Casual Dired&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;dired-do-find-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-dired-tmenu › (/) Search/Replace › (g) Find regex…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Dired-Search-_0026-Replace-_0028Search-_0026-Replace_203a_0029.html"&gt;Casual Dired&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;dired-do-search&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-dired-tmenu › (/) Search/Replace › (s) Search first regexp match…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Dired-Search-_0026-Replace-_0028Search-_0026-Replace_203a_0029.html"&gt;Casual Dired&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;dired-do-find-regexp-and-replace&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-dired-tmenu › (/) Search/Replace › (G) Find regex and replace…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/Dired-Search-_0026-Replace-_0028Search-_0026-Replace_203a_0029.html"&gt;Casual Dired&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-isearch&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-ibuffer-tmenu › (C-s) I-Search…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/IBuffer-Usage.html"&gt;Casual IBuffer&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-isearch-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-ibuffer-tmenu › (C-M-s) I-Search Regexp…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/IBuffer-Usage.html"&gt;Casual IBuffer&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-occur&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-ibuffer-tmenu › (O) Occur…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/IBuffer-Usage.html"&gt;Casual IBuffer&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-query-replace&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-ibuffer-tmenu › (M-r) Query Replace…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/IBuffer-Usage.html"&gt;Casual IBuffer&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;ibuffer-do-query-replace-regexp&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(C-o) casual-ibuffer-tmenu › (C-M-r) Query Replace Regexp…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Available via &lt;a href="https://kickingvegas.github.io/casual/IBuffer-Usage.html"&gt;Casual IBuffer&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;vc-git-grep&lt;/td&gt;
&lt;td style="text-align: left;"&gt;(Menu-bar) Tools › Version Control &amp;gt; Git grep…&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Refactoring Guidance&lt;/h1&gt;
&lt;p&gt;The replace commands can do catastrophic damage if not used with caution. Treat them like power tools.&lt;/p&gt;
&lt;p&gt;Some guidance before using replace commands:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Have a backup plan in case you need to recover anything.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;code&gt;re-builder&lt;/code&gt; to help figure out the right regexp.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Know the scope of what you want to change (directory, files, buffers).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Some commands will only modify the buffer of an affected file. Check if modified buffers are saved. You can use IBuffer to identify and save such buffers. It is left as an exercise to the reader to determine which commands exhibit this behavior.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Closing Thoughts&lt;/h1&gt;
&lt;p&gt;The inventory of commands above are from what I know of Emacs, which is guaranteed to not be comprehensive. If there are others missing from this list, please let me know at &lt;a href="mailto:kickingvegas@gmail.com?subject=Blog%20Post:%20Bulk%20Search%20and%20Replace"&gt;kickingvegas@gmail.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While many of these commands have been in Emacs for years (decades even), they were not usable to me until I built a UI for them. I think motivated readers using Casual and Anju for bulk search and/or replace tasks will find a similar sentiment.&lt;/p&gt;
&lt;hr/&gt;

&lt;p&gt;&lt;em&gt;Amended 2026-04-30&lt;/em&gt;: Added &lt;code&gt;vc-git-grep&lt;/code&gt;, &lt;code&gt;eglot-rename&lt;/code&gt;,  &lt;code&gt;xref-find-references-and-replace&lt;/code&gt;, and &lt;code&gt;xref-query-replace-in-results&lt;/code&gt;. Thanks to &lt;a href="https://social.lol/@hl/116492031100071433"&gt;Henry Leach&lt;/a&gt; and &lt;a href="https://toot.cat/@etenil/116492135347675653"&gt;Gene Pasquet&lt;/a&gt; for their input!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Amended 2026-05-04&lt;/em&gt;: Added &lt;code&gt;dired-do-replace-regexp-as-diff&lt;/code&gt;, &lt;code&gt;multi-file-replace-regexp-as-diff&lt;/code&gt;, &lt;code&gt;multi-isearch-files&lt;/code&gt;, &lt;code&gt;multi-isearch-files-regexp&lt;/code&gt;, &lt;code&gt;multi-isearch-buffers&lt;/code&gt;, &lt;code&gt;multi-isearch-buffers-regexp&lt;/code&gt;, &lt;code&gt;multi-occur&lt;/code&gt;, &lt;code&gt;multi-occur-in-matching-buffers&lt;/code&gt;. Learned about the diff commands from Christian Tietze's post &lt;a href="https://christiantietze.de/posts/2025/02/preview-mass-text-replacements-with-emacs-30-1-replace-regexp-as-diff/"&gt;Preview Mass Text Replacements with Emacs 30.1 replace-regexp-as-diff&lt;/a&gt;.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/></entry><entry><title>Some nice to know keybindings when using the mouse in Emacs</title><link href="http://yummymelon.com/devnull/some-nice-to-know-keybindings-when-using-the-mouse-in-emacs.html" rel="alternate"/><published>2026-04-24T13:40:00-07:00</published><updated>2026-04-24T13:40:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-04-24:/devnull/some-nice-to-know-keybindings-when-using-the-mouse-in-emacs.html</id><summary type="html">&lt;p&gt;Some keybindings to know whenever you’re working with a mouse in Emacs.&lt;/p&gt;</summary><content type="html">&lt;p&gt;These keybindings are good to know when working with a mouse in Emacs.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;F10&lt;/code&gt; (&lt;code&gt;menu-bar-open&lt;/code&gt;) :: Start key navigation of the menu bar in FRAME.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;S-F10&lt;/code&gt; (&lt;code&gt;context-menu-open&lt;/code&gt;) :: Start key navigation of the context menu. This needs &lt;code&gt;context-menu-mode&lt;/code&gt; enabled.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;C-M-mouse-1&lt;/code&gt; :: Activate a rectangular region around the text selected by dragging. Useful for &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Rectangles.html"&gt;rectangle&lt;/a&gt; operations.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;M-Drag-mouse-1&lt;/code&gt; ::  Set the secondary selection, with one end at the place where you press down the button, and the other end at the place where you release it. See &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Secondary-Selection.html"&gt;Secondary Selection&lt;/a&gt; for more info.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;C-mouse-3&lt;/code&gt;:: Raise menu populated with the menu mode map of the current mode and if Imenu is enabled, &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Imenu.html"&gt;an index menu for that buffer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;C-u&lt;/code&gt; &lt;em&gt;n (optional)&lt;/em&gt;  :: A &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Prefix-Command-Arguments.html"&gt;prefix argument&lt;/a&gt; can be passed to a menu item.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Keyboard Macro&lt;/em&gt; :: Commands issued via menu can be used when recording a &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Keyboard-Macros.html"&gt;keyboard macro&lt;/a&gt;.  (🤯 Who knew?)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;C-x z&lt;/code&gt; (&lt;code&gt;repeat&lt;/code&gt;) :: Repeat most recently executed command. Yes, &lt;code&gt;repeat&lt;/code&gt; works also for commands issued via mouse too!&lt;/p&gt;
&lt;p&gt;Knowing that &lt;code&gt;repeat&lt;/code&gt; works with menu commands (both main and context) is particularly useful with the additions to the main menu made by the latest &lt;a href="https://github.com/kickingvegas/anju/discussions/63"&gt;Anju v1.2.0 update&lt;/a&gt;. Shown below is a demo of using “Duplicate” (&lt;code&gt;duplicate-dwim&lt;/code&gt;) from the Edit menu, then using &lt;code&gt;C-x z&lt;/code&gt; and then &lt;code&gt;z&lt;/code&gt; successively to repeat the duplication.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;video controls  muted=true poster="http://yummymelon.com/devnull/images/mouse-keybindings/anju-duplicate-demo-thumb.png" width="75%"&gt;
&lt;source src="http://yummymelon.com/devnull/images/videos/anju-duplicate-demo-720.mov" type="video/mp4" /&gt;
&lt;p&gt;
Your browser does not support HTML video. &lt;a href="http://yummymelon.com/devnull/images/videos/anju-duplicate-demo-720.mov" download="{static}images/videos/anju-duplicate-demo-720.mov"&gt;Link to video&lt;/a&gt;.
&lt;/p&gt;
&lt;/video&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Amended 2026-04-25&lt;/em&gt;:  Added &lt;code&gt;C-M-mouse-1&lt;/code&gt;, &lt;code&gt;M-Drag-mouse-1&lt;/code&gt;, &lt;code&gt;C-mouse-3&lt;/code&gt;, &lt;code&gt;C-u n&lt;/code&gt;, &lt;em&gt;Keyboard Macro&lt;/em&gt; items.&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="anju"/></entry><entry><title>Call for Testing: Scrim v1.1.3 TestFlight on pre-release Emacs 31</title><link href="http://yummymelon.com/devnull/call-for-testing-scrim-113-testflight-on-pre-release-emacs-31.html" rel="alternate"/><published>2026-04-23T14:40:00-07:00</published><updated>2026-04-23T14:40:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-04-23:/devnull/call-for-testing-scrim-113-testflight-on-pre-release-emacs-31.html</id><summary type="html">&lt;p&gt;Looking for folks who want to test a new pre-release build of Scrim v1.1.3 which fixes it for Emacs 31.&lt;/p&gt;</summary><content type="html">&lt;p&gt;For folks who like using the development build of Emacs (at the time of this writing Emacs 31.0.5) on macOS, it turns out there is a &lt;a href="https://github.com/kickingvegas/scrim/issues/76"&gt;bug&lt;/a&gt; in how &lt;a href="http://yummymelon.com/scrim/"&gt;Scrim&lt;/a&gt; sends Org protocol requests to it.&lt;/p&gt;
&lt;p&gt;A fix has been identified and so far it works fine for both Emacs 30.2 and 31.0.5 from my testing. That said, it is always better to have more folks trying it out.&lt;/p&gt;
&lt;p&gt;If you use the latest Emacs 31.0.5+ on macOS and use (or are interested in using) Org protocol, I invite you to &lt;a href="https://testflight.apple.com/join/5HNzNtft"&gt;try out a TestFlight build of Scrim v1.1.3&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this is the first time you’ve heard about Scrim, learn more about it at &lt;a href="http://yummymelon.com/scrim/"&gt;http://yummymelon.com/scrim/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks much!&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="org mode"/><category term="scrim"/><category term="macos"/></entry><entry><title>Computing Days Until with Emacs</title><link href="http://yummymelon.com/devnull/computing-days-until-with-emacs.html" rel="alternate"/><published>2026-04-08T16:00:00-07:00</published><updated>2026-04-08T16:00:00-07:00</updated><author><name>Charles Choi</name></author><id>tag:yummymelon.com,2026-04-08:/devnull/computing-days-until-with-emacs.html</id><summary type="html">&lt;p&gt;Countdown clocks are always useful. Here’s one for computing days until in Emacs.&lt;/p&gt;</summary><content type="html">&lt;p&gt;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 &lt;code&gt;org-time-stamp-to-now&lt;/code&gt; doing the timestamp computation for you.&lt;/p&gt;
&lt;p&gt;We can wrap &lt;code&gt;org-time-stamp-to-now&lt;/code&gt; in an internal function &lt;code&gt;cc/--days-until&lt;/code&gt; that generates a formatted string of the days until a target date.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cc/--days-until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;optional&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Formatted string of days until TARGET.&lt;/span&gt;

&lt;span class="s"&gt;- TARGET: date string that conforms to &lt;/span&gt;&lt;span class="ss"&gt;`parse-time-string&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;
&lt;span class="s"&gt;- TEMPLATE : format string that includes ‘%d’ specifier.&lt;/span&gt;

&lt;span class="s"&gt;If TEMPLATE is nil, then a predefined format string will be&lt;/span&gt;
&lt;span class="s"&gt;used.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;template&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="nv"&gt;template&lt;/span&gt;
&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%d days until &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-time-stamp-to-now&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;From there we can then start defining commands that use &lt;code&gt;cc/--days-until&lt;/code&gt;. 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 &lt;code&gt;org-read-date&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cc/days-until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Prompt user for date and show days until in the mini-buffer.&lt;/span&gt;

&lt;span class="s"&gt;Use &lt;/span&gt;&lt;span class="ss"&gt;`org-read-date&amp;#39;&lt;/span&gt;&lt;span class="s"&gt; to compute days until to display in the mini-buffer.&lt;/span&gt;

&lt;span class="s"&gt;If prefix ARG is non-nil, then the computed result is stored in the&lt;/span&gt;
&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="ss"&gt;`kill-ring&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interactive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;P&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-read-date&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cc/--days-until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;arg&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;kill-new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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 &lt;code&gt;cc/days-until-voting&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cc/days-until-voting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Days until U.S. elections in 2026 and 2028.&lt;/span&gt;

&lt;span class="s"&gt;If prefix ARG is non-nil, then the computed result is stored in the&lt;/span&gt;
&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="ss"&gt;`kill-ring&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interactive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;P&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;midterms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cc/--days-until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2026-11-03&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%d days until 2026 midterms&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;election&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cc/--days-until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2028-11-07&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%d days until 2028 presidential election&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%s, %s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;midterms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;election&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;arg&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;kill-new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The result of &lt;code&gt;M-x cc/days-until-voting&lt;/code&gt; as of 8 April 2026 is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;209 days until 2026 midterms, 944 days until 2028 presidential election&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;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!&lt;/p&gt;</content><category term="misc"/><category term="emacs"/><category term="org mode"/></entry></feed>