Jeremy Friesen: Mythic Bastionland Map Play Aid Emacs Package

In the Forge from the Worst series, I’ve been writing about my solo play in Mythic Bastionland 📖 . I had went into the game hoping to engage the map without referee knowledge of the map; no insight into where all the features are, but instead allowing the procedures of the game to reveal the locations of various features.

Midway through my 3rd session, I found myself needing to reveal information of the map that was going to be more challenging to derive. I chose that moment—a just in time moment—to set about loading the map into Emacs 📖 and then write commands to interrogate the map.

I reviewed the questions I had already asked of the map, and wrote them down:

  • What is the direction to a named Myth?
  • What is the nearest Myth?
  • What is a random Myth that is not the nearest?
  • Is there a barrier when moving between these two hexes?
  • Is there a Myth on this Hex?
  • What is feature is there on this hex?

I suspect I may also need to ask: “What is the direction to the nearest Myth?”

I wasn’t going to type up the map and store it, but instead randomly generate the map. Also, I wanted to make sure that I could generate a map that would conform to the player-discovered information from the sessions thus far session.

Initial Research

Given that I was going to be working out distances, I read through the amazing and helpful Hexagonal Grids.

I needed to settle on an appropriate coordinates systems. With some reading, I chose to adopt the double-height coordinates. Which was not the notation I had already written down in blog posts, but would be easy to map.

Initial Work

I knew that once I created a map that conformed to Sir Weydlyn’s observations, I wouldn’t want to dig into the data nor debug functions using that map. So, my approach was to work from a disposable map and test the functions.

I used the Read-eval-print loop (REPL 📖) to test these functions. One emergent foible is that I kept using the single-height coordinates; introducing a translating function for internal work. As of this foible persists. And creates a bit more chatter, which we’ll see in a bit.

Encoding the Known World

Once I had a solid set of functions that could answer questions, I set about encoding the Known World. Let’s walk through this.

First we have this mbc function. It converts single-height coordinates to double-height such that (mbc 8 1) will be (8 . 2) and (mbc 7 1) will be (7 . 3).

(defalias 'mbc 'mythic-bastionland--random-coord)

Next let’s read over the code and comments. I’ll meet you after the code block.

(defalias 'mbc 'mythic-bastionland--random-coord)
(mythic-bastion-land-map-write
 (mythic-bastionland-map-generate
  `((constraints .
                 ((nearest . ((label . "The Mountain")
                              (feature . myths)
                              (coord . ,(mbc 9 1))))
                  (nearest . ((label . "The Judge")
                              (feature . myths)
                              (coord . ,(mbc 7 2))))))
    ;; This is where Sir Weydlyn encountered Seer Tompot.
    (sanctums . (("Tompot (Tangled Seer)" . ,(mbc 8 1))))
    ;; With the chosen random scenario, we assign the Moutain, then
    ;; pick a random one for the Beast
    (myths . (("The Mountain" .
               (,(mbc 10 4) ,(mbc 8 4) ,(mbc 9 2)
                ,(mbc 9 3) ,(mbc 9 4) ,(mbc 9 5)))
              ("The Beast" .
               (,(mbc 8 3) ,(mbc 9 3) ,(mbc 8 4)
                ,(mbc 9 4) ,(mbc 8 5)))
              ("The Judge" .
               ,(mythic-bastionland-hexes-within-range
                 (mbc 7 2) 3))))
    ;; These have been converted to double height coordinates.
    (holdings . (("Tower" . (9 . 3)) ("Castle" . (5 . 7))
                 ("Fortress" . (1 . 19)) ("Town" . (8 . 16))))
    (omens-revealed . (("The Mountain" . 1)))
    (omit (
           ;; Sir Wedylyn crossed between these two potential
           ;; barriers.
           (barriers .
                     ((,(mbc 8 1) . ,(mbc 9 1))
                      (,(mbc 9 1) . ,(mbc 8 2))
                      (,(mbc 8 2) . ,(mbc 7 2)))))))))

The mythic-bastionland-map-generate function takes an alist with keys: holdings, myths, landmarks, dwellings, sanctums, monuments, hazards, curses, ruins, barriers, constraints, omens-revealed, and omit.

This allows me to specify where to place specific already known landmarks as well as to omit placing landmarks at a given coordinate.

Of those: holdings, myths, landmarks, dwellings, sanctums, monuments, hazards, curses, and ruins are nameable feature types; a function I wanted as I managed the map.

The omit option allows me to specify coordinates that I will not place the named feature.

The constraints option are tests that must be true with the completed map. When all of them are not true, I discard that generated map and create another one. By default this will be attempted 10 times; but you can also pass max-retries to the generation to modify that amount.

And omens-revealed allows for tracking of each omen’s present state.

Generating the World from that Which is Known

With the mythic-bastionland-features, I define the feature types (that are renamable), how many there should be, and optionally a minimum distance.

 (defvar mythic-bastionland-features
  '((myths . ((how-many . (6))))
     (holdings . ((how-many . (4)) (min-distance . 5)))
     (sanctums . ((how-many . (3 4))))
     (monuments . ((how-many . (3 4))))
     (dwellings . ((how-many . (3 4))))
     (hazards . ((how-many . (3 4))))
     (curses . ((how-many . (3 4))))
     (ruins . ((how-many . (3 4)))))
  "Feature types that are labeled, and thus renameable.  Also we want
to consider how many of these we might place as well as the minimum
distance (if any).")

For holdings, there are 4 at a minimum distance of 5. For myths there are always 6. And for the others, there is either 3 or 4.

And now we have the code that generates the map based on the given configuration.

In short:

  1. We place the given features; when a feature has more than one coordinate, we randomly pick a coordinate that does not have something in it.
  2. Build a list of the remaining features to place.
  3. Looping through the locations to place, attempt to place them on the map; honoring minimum distance, omitted coordinates, as well as only allowing one feature per hex.
  4. Randomly place known barriers, skipping over omitted ones. (As of , I don’t have a means of placing known barriers, but that feature is trivial to add).
  5. Validate that all constraints are true; if not, try again.
  6. The record any omens-revealed.
(defun mythic-bastionland-map-generate (config)
  "Generate and store `mythic-bastionland-map' via CONFIG.

See `mythic-bastionland-features' for some of the `car' values of
CONFIG.  Another is `barriers' (which are unamed).  Another is `omit',
itself an alist, with the same `car' values as those in CONFIG (except
`omit').

When providing existing locations to place, you may provide either a
single coordinate or a list of coordinates (from which the function will
randomly pick a candidate of coordinates not already placed).  The logic
enforces that only one feature may be placed in each hex.

Given this placement logic, ensure that the config places features with
less candidate spaces earlier."
  (let ((max-retries
          (or (alist-get 'max-retries config) 10))
         (keep-mapping t)
         (the-map nil))
    (while (and keep-mapping (> max-retries 0))
      ;; Assume that we don't need to keep trying to build the map
      (setq keep-mapping nil)
      (setq the-map nil)
      (setq max-retries (- max-retries 1))

      ;; Now, let's see if our assumption is correct.
      (let ((barriers nil)
             (locations nil)
             (locations-to-place nil))
        ;; First put the locations on the map...no effort is taken to
        ;; avoid location collisions.  Also, queue up further locations
        ;; to place.
        (cl-loop for (feature . fconfig) in mythic-bastionland-features do
          (let* ((feat-locations
                   (alist-get feature config))
                  (how-many
                    (alist-get 'how-many fconfig))
                  ;; TODO: allow for multiple feature entries.
                  (placed-features '()))

            ;; When we are given location qs for this feature type, add
            ;; it to the placed list.
            (when feat-locations
              (cl-loop for (label . list-or-one-coord) in feat-locations do
                (let ((placed-coordinates
                        (mapcar #'car locations)))
                  (if (consp (car list-or-one-coord))
                    ;; We have a list of coordinates
                    (let ((coord
                            (seq-random-elt
                              (seq-filter
                                (lambda (c)
                                  (not (member c placed-coordinates)))
                                list-or-one-coord))))
                      (unless coord
                        (user-error "Location %s with coordinate options %s cannot be placed due to collisoin with all other placed locations."
                          (label list-or-one-coord)))
                      (cl-pushnew (cons coord label) locations)
                      (cl-pushnew (cons label coord) placed-features))
                    ;; We have one coordinate
                    (let ((coord list-or-one-coord))
                      (when (member coord placed-coordinates)
                        (user-error "Location %s with coord %s cannot be placed due to existing placed location"
                          label coord))
                      (cl-pushnew (cons coord label) locations)
                      (cl-pushnew (cons label coord) placed-features))))))
            (cl-pushnew (cons feature placed-features) the-map)

            ;; Next queue up placing the remainder of locations for the
            ;; feature type (accounting for what was already given).
            (dotimes (i (- (seq-random-elt how-many)
                          (length feat-locations)))
              (cl-pushnew (cons feature
                            (format "%s %s" feature (+ i 1)))
                locations-to-place))))

        ;; Now that we have our task list of what all needs adding.
        ;;
        ;; This involves avoiding collisions with other placed features
        ;; as well as heading the guidance of an omit coordinates for
        ;; the given feature.
        (cl-loop for (feature . label) in locations-to-place do
          (let ((keep-trying t)
                 (min-distance
                   (alist-get 'min-distance
                     (alist-get feature mythic-bastionland-features)))
                 (omitted-feature-coordinates
                   (alist-get feature (alist-get 'omit the-map))))
            (while keep-trying
              (let* ((coord
                       (mythic-bastionland--random-coord)))
                (when (and
                        ;; Verify that what we're placing is place at
                        ;; the minimum distance.
                        (if min-distance
                          (<= min-distance
                            (min
                              (mapcar
                                (lambda (label-coord)
                                  (mythic-bastionland--hex-distance
                                    coord (cdr label-coord)))
                                (alist-get feature the-map))))
                          t)
                        (not (or
                               (assoc coord locations)
                               (member coord
                                 omitted-feature-coordinates))))
                  (progn
                    (setq keep-trying nil)
                    ;; For locations we favor storing the (coord . label)
                    ;; This makes later comparisons easier.n
                    (cl-pushnew (cons coord label) locations)
                    ;; For a named feature favor storing (label . coord)
                    ;; as this makes prompts easier.
                    (cl-pushnew (cons label coord)
                      (alist-get feature the-map))))))))

        ;; Nex, we handle the barriers as they are a bit of a different
        ;; creature.  We generate them by placing them between two
        ;; neighboring hexes.
        ;;
        ;; I have given special consideration for hexes on the edge of
        ;; the map; Namely don't create barriers on the edges.  And
        ;; proportionally reduce the chance of adding a barrier on those
        ;; edges proportional to the number sides that the hex has
        ;; on-map neighbors."
        (let ((omitted-barriers
                (mapcar
                  (lambda (b)
                    (mythic-bastionland--make-ordered-pair
                      (car b) (cdr b)))
                  (alist-get 'barriers (alist-get 'omit config)))))
          (dotimes (i (+ 23 (random 3)))
            (let ((keep-trying t))
              (while keep-trying
                (let* ((coord
                         (mythic-bastionland--random-coord))
                        (in-6-chance
                          (cond
                            ((member coord '((0 . 0) (11 . 22)))
                              ;; top-left, bottom-right
                              2)
                            ((member coord '((11 . 0) (0 . 22)))
                              ;; top-right, bottom-right
                              3)
                            ((member (car coord) '(0 11))
                              ;; from or to
                              4)
                            ((member (cdr coord) '(0 23))
                              ;; top of col that is taller; bottom of
                              ;; col that is shorter
                              3)
                            ((member (cdr coord) '(1 22))
                              ;; top of col that is shorter; bottom of
                              ;; col that is taller
                              5)
                            (t 6))))
                  (when (<= (+ 1 (random 6)) in-6-chance)
                    (progn
                      (let* ((neighbor
                               (seq-random-elt
                                 (mythic-bastionland--neighbors coord)))
                              (pair
                                (mythic-bastionland--make-ordered-pair
                                  coord neighbor)))
                        ;; Don't repeat barriers
                        (when
                          (not (or (member pair barriers)
                                 (member pair omitted-barriers)))
                          (progn
                            (cl-pushnew pair barriers)
                            (setq keep-trying nil)))))))))))

        ;; PS...make sure we add the locations and barriers to the map.
        (cl-pushnew `(locations . ,locations) the-map)
        (cl-pushnew `(barriers . ,barriers) the-map)

        ;; Next, see if we have a conformant map
        (cl-loop
          for (constraint . info)
          in (alist-get 'constraints config) do
          (pcase constraint
            ('nearest
              (unless (mythic-bastionland--test-constraint-nearest info the-map)
                (setq keep-mapping t)))
            (_ (user-error "Unknow constraint %s" constraint))))))

    ;; TODO: Consider generalizations but for now this is adequate.
    (when-let ((omens-revealed (assoc 'omens-revealed config)))
      (push omens-revealed the-map))
    the-map))

It’s All a Mapping Problem

I once read that all computer science problems are mapping problems. There were two that I needed to consider.

First, when rolling up a barrier, it is placed along the shared side of two adjacent hexes. This meant creating a “unique key” for those pairs, so that I don’t accidentally pix Hex 0,0 then its neighbor Hex 0,1 to place a barrier, and then pick Hex 0,1 and its neighbor Hex 0,0 to place a hex.

Enter the mythic-bastionland--make-ordered-pair function:

(defun mythic-bastionland--make-ordered-pair (from to)
  "Provide a consistent sort order FROM and TO coordinates."
  (let ((from
          (or from
            (mythic-bastionland--text-to-coord nil "Left ")))
         (to
           (or to
             (mythic-bastionland--text-to-coord nil "Right "))))
    (if (> (car from) (car to))
      `(,from . ,to)
      (if (> (cdr from) (cdr to))
        `(,from . ,to)
        `(,to . ,from)))))

It normalizes a pair of coordinates so that we can have consistent interaction when referencing those two coordinates.

Second, one of the questions was direction from one coordinate to another. And here we have mythic-bastionland--direction:

(defun mythic-bastionland--direction (&optional from to)
  "Get human-readable direction FROM TO."
  (let ((from
          (or from
            (mythic-bastionland--text-to-coord nil "From ")))
         (to
           (or to
             (mythic-bastionland--text-to-coord nil "To "))))
      (cond
        ((equal to from)
          "Under your nose")
        ((= (car from) (car to))
          (if (> (cdr from) (cdr to))
            "North" "South"))
        (t (let ((slope (/
                          (float (- (cdr to) (cdr from)))
                          (float (- (car to) (car from))))))
             (cond
               ;; After compass, protractor, marker, and spreadsheet
               ;; work, I'm happy with the direction calculations.
               ;; Remember, hex maps starting from top-left instead
               ;; of bottom right like Geometry means things get a
               ;; mind bending (at least for this old guy).
               ((or (> slope 4) (< slope -4))
                 (if (> (cdr from) (cdr to))
                   "North" "South"))
               ((<= 0.8 slope) (<= slope 4)
                 (if (> (cdr from) (cdr to))
                   "Northwest" "Southeast"))
               ((< -0.8 slope 0.8)
                 (if (> (car from) (car to))
                   "West" "East"))
               ((<= -4 slope -0.8)
                 (if (> (cdr from) (cdr to))
                   "Northeast" "Southwest"))))))))

Using geometry of Rise over Run to determine slope, I can enter the from and to coordinate to get the named direction. As the comments indicate, this involved some protractor work to make sure I got the algorithm correct.

When I had set out, I had first thought of saying the two adjacent hexes to the right of a given hex were to the given hex’s east. But the geometry suggested breaking this apart.

So for a given Hex, and looking at Hexes one space away and starting at the Hex directly above the given Hex and working clockwise we have: north, north east, south east, south, south west, and north west. Stepping to the next ring out, we have: north, north east, north east, west, south east, south east, south, south west, south west, west, north west, north west.

I felt that having the algorithm well understood by me would make for consistent solo play.

Testing This Thing

When you look at the code, you might notice that the interactive functions will take a coordinate or prompt you for one. You might also note that some non-interactive functions take optional coordinates, and prompt if none are given.

All of this was in service of attempting to test functions. Verifying the correctness of distance and direction required no knowledge of the map, but instead relied on two coordinates. So I could bombard these functions in the REPL and prompt for the inputs.

I also made a decision not to codify Sir Weydlyn’s map until I’d test driven things a few times. Hence I write the map to disk and then read it back when I want to use it. This also serves to clobber the variable’s value, preventing accidental peaking. I am considering further measures, but am holding off.

I found that once it was all tested, and I started playing, I realized I wanted to adjust some functionality. Namely, determining the nearest myth. I refactored that section. And to test, used a bit of dependency injection, to pass in the map I wanted to test (so as to not peak).

Once I verified behavior, I loaded the game map and made sure the question I had previously asked of the map returned the same result. And it didn’t so I set about further refactoring (which added the constraints option). I adjusted the initial config, adding constraints and allowing for features to be placed from a subset of coordinates.

Conclusion

I’ve had two significant refactors of the base functionality.

From the initial state to the next state, I needed to consider that I was changing the logic for what was nearest, going from randomly picking hexes that had the same distance to now consistently picking hexes.

I performed the refactor then tested my map. The answer I got conflicted with established facts (e.g. “The Judge” was the closest myth to 9,1). So I needed to refactor again. This is when I introduced constraints. Which was relatively easy to introduce.

I did the second refactor in two parts. Part 1 was wrapping the existing body in the following then re-indenting:

(let ((max-retries
       (or (alist-get 'max-retries config) 10))
      (keep-mapping t)
      (the-map nil))
  (while (and keep-mapping (> max-retries 0))
    ;; Assume that we don't need to keep trying to build the map
    (setq keep-mapping nil)
    (setq the-map nil)
    (setq max-retries (- max-retries 1))

    …EXISTING BODY…
    ))

This change was a noop change, that I committed. Then I set about implementing the constraints logic. This way I would have smaller second commit that didn’t interweave with indentation changes.

You can find the “mythic-bastionland” package on Sourcehut.

-1:-- Mythic Bastionland Map Play Aid Emacs Package (Post Jeremy Friesen)--L0--C0--2025-12-17T01:11:00.000Z

Irreal: Handwritten Notes With Emacs

Just a quickie today. This post addresses a niche need that most readers probably aren’t facing. Joar Alexander Pablo von Arndt’s girlfriend recently bought a Framework 12 laptop and stylus and wanted to use it to take handwritten notes that she could later organize in Org mode.

That’s the type of thing that sounds like it should be doable with Emacs and, of course, it is. The solution they settled on was to use Xournal++ to take the handwritten notes and org-xournalpp to interface it to Emacs.

Sadly, it didn’t work quite right on the Framework 12 so they wrote a bit of Elisp to do the interfacing. It turned out to be pretty easy. See Pablo von Arndt’s post for the details.

This is another nice example of how you can use Emacs to solve problems with just a little investigation into what’s available and perhaps a bit of Elisp.

-1:-- Handwritten Notes With Emacs (Post Irreal)--L0--C0--2025-12-16T16:24:31.000Z

I think I’m going back to clocking in and out of sub-tasks, even though I said I wouldn’t. It’s easier not to, but the end result, I think, is messier, and I can’t get the clock reports right… on the other hand, it’s kind of an overkill. 🤔

-1:--  (Post TAONAW - Emacs and Org Mode)--L0--C0--2025-12-16T13:31:07.000Z

James Dyer: Setting Up Emacs for C# Development on Windows

Introduction

I have been developing C# with .NET 9.0 for the last year on Windows and I thought it was probably time to write down my current setup, and maybe someone might even find this useful!

So, this guide documents my setup for running Emacs 30.1 on Windows with full C# development support, including LSP, debugging (through DAPE), and all the ancillary tools you’d expect from a modern development environment. The setup is designed to be portable and self-contained, which is particularly useful in air-gapped or restricted environments.

A version of this can be found at https://github.com/captainflasmr/Emacs-on-windows which will be a living continually updated version!

Prerequisites

Before we begin, you’ll need:

  • Windows 10 or 11 (64-bit)
  • .NET 9.0 SDK - Required for csharp-ls and building .NET projects
  • Visual Studio 2022 (optional) - Useful for MSBuild and if you need the full IDE occasionally
  • Administrator access - For initial setup only

You can verify your .NET installation by opening a command prompt and running:

dotnet --version

If you see version 9.0.x or later, you’re ready to proceed.

The Big Picture

Here’s what we’re building:

D:\source\emacs-30.1\
├── bin\
│   ├── emacs.exe, runemacs.exe, etc.
│   ├── PortableGit\          # Git for version control
│   ├── Apache-Subversion\    # SVN (if needed)
│   ├── csharp-ls\            # C# Language Server
│   ├── netcoredbg\           # .NET debugger
│   ├── omnisharp-win-x64\    # Alternative C# LSP
│   ├── hunspell\             # Spell checking
│   ├── find\                 # ripgrep for fast searching
│   ├── ffmpeg-7.1.1-.../     # Video processing
│   └── ImageMagick-.../      # Image processing
└── share\
    └── emacs\...

The key insight here is keeping everything within the Emacs installation directory. This makes the whole setup portable—you can copy it to another machine or keep it on a USB drive.

Step 1: Installing Emacs

Download Emacs 30.1 from the GNU Emacs download page. For Windows, grab the installer or the zip archive.

I install to an external drive D:\source\emacs-30.1 rather than Program Files—it avoids permission issues and keeps everything in one place.

Test your installation by running bin\runemacs.exe. You should see a fresh Emacs frame.

Step 2: Setting Up csharp-ls (The C# Language Server)

This is the heart of the C# development experience. csharp-ls provides code completion, go-to-definition, find references, diagnostics, and more through the Language Server Protocol (LSP).

Option A: Installing via dotnet tool (Recommended for Internet Access)

If you have internet access, the easiest way to install csharp-ls is as a .NET global tool:

# Install the latest version globally
dotnet tool install --global csharp-ls

# Or install a specific version
dotnet tool install --global csharp-ls --version 0.20.0

# Verify installation
csharp-ls --version

By default, global tools are installed to:

  • Windows: %USERPROFILE%\.dotnet\tools

The executable will be csharp-ls.exe and can be called directly once the tools directory is in your PATH.

Option B: Offline Installation via NuGet Package

For air-gapped environments, you can download the NuGet package and extract it manually:

  1. On a machine with internet, download the package:

          # Download the nupkg file
          nuget install csharp-ls -Version 0.20.0 -OutputDirectory ./packages
    
          # Or download directly from NuGet Gallery:
          # https://www.nuget.org/packages/csharp-ls/
          # Click "Download package" on the right side
    
  2. The .nupkg file is just a ZIP archive. Extract it:

          # Rename to .zip and extract, or use 7-Zip
          # The DLLs are in tools/net9.0/any/
    
  3. Copy the tools/net9.0/any/ directory to your Emacs bin:

          xcopy /E packages\csharp-ls.0.20.0\tools\net9.0\any D:\source\emacs-30.1\bin\csharp-ls\
    
  4. The language server is now at: D:\source\emacs-30.1\bin\csharp-ls\CSharpLanguageServer.dll

Configuring Eglot for csharp-ls

In your init.el, configure Eglot to use csharp-ls:

(require 'eglot)

;; Option A: If installed as a global tool
(setq eglot-server-programs
      '((csharp-mode . ("csharp-ls"))))

;; Option B: If running from extracted DLL
(setq eglot-server-programs
      '((csharp-mode . ("dotnet"
                        "D:/source/emacs-30.1/bin/csharp-ls/CSharpLanguageServer.dll"))))

I also have the following commented out if there are some eglot functions that causes slowdowns or I just think I don’t need:

(setq eglot-ignored-server-capabilities
      '(
        ;; :hoverProvider                    ; Documentation on hover
        ;; :completionProvider               ; Code completion
        ;; :signatureHelpProvider            ; Function signature help
        ;; :definitionProvider               ; Go to definition
        ;; :typeDefinitionProvider           ; Go to type definition
        ;; :implementationProvider           ; Go to implementation
        ;; :declarationProvider              ; Go to declaration
        ;; :referencesProvider               ; Find references
        ;; :documentHighlightProvider        ; Highlight symbols automatically
        ;; :documentSymbolProvider           ; List symbols in buffer
        ;; :workspaceSymbolProvider          ; List symbols in workspace
        ;; :codeActionProvider               ; Execute code actions
        ;; :codeLensProvider                 ; Code lens
        ;; :documentFormattingProvider       ; Format buffer
        ;; :documentRangeFormattingProvider  ; Format portion of buffer
        ;; :documentOnTypeFormattingProvider ; On-type formatting
        ;; :renameProvider                   ; Rename symbol
        ;; :documentLinkProvider             ; Highlight links in document
        ;; :colorProvider                    ; Decorate color references
        ;; :foldingRangeProvider             ; Fold regions of buffer
        ;; :executeCommandProvider           ; Execute custom commands
        ;; :inlayHintProvider                ; Inlay hints
        ))

Step 3: Setting Up the Debugger (netcoredbg)

For debugging .NET applications, we’ll use netcoredbg, which implements the Debug Adapter Protocol (DAP).

Installing netcoredbg

  1. Download from Samsung’s GitHub releases
  2. Extract to D:\source\emacs-30.1\bin\netcoredbg\
  3. Verify: netcoredbg.exe --version

Configuring dape for Debugging

dape is an excellent DAP client for Emacs. Here’s my configuration:

(use-package dape
  :load-path "z:/SharedVM/source/dape-master"
  :init
  ;; Set key prefix BEFORE loading dape
  (setq dape-key-prefix (kbd "C-c d"))
  :config
  ;; Define common configuration
  (defvar project-netcoredbg-path "d:/source/emacs-30.1/bin/netcoredbg/netcoredbg.exe"
    "Path to netcoredbg executable.")
  (defvar project-netcoredbg-log "d:/source/emacs-30.1/bin/netcoredbg/netcoredbg.log"
    "Path to netcoredbg log file.")
  (defvar project-project-root "d:/source/PROJECT"
    "Root directory of PROJECT project.")
  (defvar project-build-config "Debug"
    "Build configuration (Debug or Release).")
  (defvar project-target-arch "x64"
    "Target architecture (x64, x86, or AnyCPU).")

  ;; Helper function to create component configs
  (defun project-dape-config (component-name dll-name &optional stop-at-entry)
    "Create a dape configuration for a component.
COMPONENT-NAME is the component directory name
DLL-NAME is the DLL filename without extension.
STOP-AT-ENTRY if non-nil, stops at program entry point."
    (let* ((component-dir (format "%s/%s" project-project-root component-name))
           (bin-path (format "%s/bin/%s/%s/net9.0"
                             component-dir
                             project-target-arch
                             project-build-config))
           (dll-path (format "%s/%s.dll" bin-path dll-name))
           (config-name (intern (format "netcoredbg-launch-%s"
                                        (downcase component-name)))))
      `(,config-name
        modes (csharp-mode csharp-ts-mode)
        command ,project-netcoredbg-path
        command-args (,(format "--interpreter=vscode")
                      ,(format "--engineLogging=%s" project-netcoredbg-log))
        normalize-path-separator 'windows
        :type "coreclr"
        :request "launch"
        :program ,dll-path
        :cwd ,component-dir
        :console "externalTerminal"
        :internalConsoleOptions "neverOpen"
        :suppressJITOptimizations t
        :requireExactSource nil
        :justMyCode t
        :stopAtEntry ,(if stop-at-entry t :json-false))))

  ;; Register all component configurations
  (dolist (config (list
                   (project-dape-config "DM" "DM.MSS" t)
                   (project-dape-config "Demo" "Demo.MSS" t)
                   (project-dape-config "Test_001" "Test" t)))
    (add-to-list 'dape-configs config))

  ;; Set buffer arrangement and other options
  (setq dape-buffer-window-arrangement 'gud)
  (setq dape-debug t)
  (setq dape-repl-echo-shell-output t))

Now you can start debugging with M-x dape and selecting your configuration.

Step 4: Installing Supporting Tools

Portable Git

  1. Download PortableGit-2.50.0-64-bit.7z.exe from git-scm.com
  2. Run and extract to D:\source\emacs-30.1\bin\PortableGit\

This gives you git.exe, bash.exe, and a whole Unix-like environment.

ripgrep (Fast Searching)

  1. Download from ripgrep releases
  2. Extract rg.exe to D:\source\emacs-30.1\bin\find\

ripgrep is dramatically faster than grep for searching codebases.

Hunspell (Spell Checking)

  1. Download hunspell-1.3.2-3-w32-bin.zip
  2. Extract to D:\source\emacs-30.1\bin\hunspell\
  3. Download dictionary files (en_GB.dic and en_GB.aff) and place in hunspell\share\hunspell\

ImageMagick (Image Processing)

  1. Download the portable Q16 x64 version from imagemagick.org
  2. Extract to D:\source\emacs-30.1\bin\ImageMagick-7.1.2-9-portable-Q16-x64\

This enables image-dired thumbnail generation.

FFmpeg (Video Processing)

  1. Download from ffmpeg.org (essentials build is fine)
  2. Extract to D:\source\emacs-30.1\bin\ffmpeg-7.1.1-essentials_build\

Useful for video thumbnails in dired and media processing.

Step 5: Configuring the PATH

This is crucial—Emacs needs to find all these tools. Here’s the PATH configuration from my init.el:

(when (eq system-type 'windows-nt)
  (let* ((emacs-bin "d:/source/emacs-30.1/bin")
         (xPaths
          `(,emacs-bin
            ,(concat emacs-bin "/PortableGit/bin")
            ,(concat emacs-bin "/PortableGit/usr/bin")
            ,(concat emacs-bin "/hunspell/bin")
            ,(concat emacs-bin "/find")
            ,(concat emacs-bin "/netcoredbg")
            ,(concat emacs-bin "/csharp-ls/tools/net9.0/any")
            ,(concat emacs-bin "/ffmpeg-7.1.1-essentials_build/bin")
            ,(concat emacs-bin "/ImageMagick-7.1.2-9-portable-Q16-x64")))
         (winPaths (getenv "PATH")))
    (setenv "PATH" (concat (mapconcat 'identity xPaths ";") ";" winPaths))
    (setq exec-path (append xPaths (parse-colon-path winPaths)))))

Step 6: Installing Emacs Packages

Extract these to a shared location or download from MELPA

Package Purpose
corfu Modern completion UI
dape Debug Adapter Protocol client
highlight-indent-guides Visual indentation guides
ztree Directory tree comparison
web-mode Web template editing

Example package configuration:

(use-package corfu
  :load-path "z:/SharedVM/source/corfu-main"
  :custom
  (corfu-auto nil)         ; Manual completion trigger
  (corfu-cycle t)          ; Cycle through candidates
  (corfu-preselect 'first))

(use-package ztree
  :load-path "z:/SharedVM/source/ztree"
  :config
  (setq ztree-diff-filter-list
        '("build" "\\.dll" "\\.git" "bin" "obj"))
  (global-set-key (kbd "C-c z d") 'ztree-diff))

(use-package web-mode
  :load-path "z:/SharedVM/source/web-mode-master"
  :mode "\\.cshtml?\\'"
  :hook (html-mode . web-mode)
  :bind (:map web-mode-map ("M-;" . nil)))

Note that I turn off autocomplete for corfu and complete using complete-symbol manually, otherwise the LSP is constantly accessed with slowdown.

I often use Meld but am currently am looking to adapt ztree to perform better for directory comparisons.

Web-mode is the best package I have found for html type file navigation and folding, very useful when developing Razor pages for example.

Step 7: auto open file modes

Of course running and building in windows means in Emacs probably having to open .csproj files from time to time, well nxml-mode comes in useful for this:

(add-to-list 'auto-mode-alist '("\\.csproj\\'" . nxml-mode))

Step 8: build script

Here is my general build script, leveraging msbuild and running generally from eshell

New projects are added to :

set PROJECTS
set PROJECT_NAMES
@echo off
setlocal

REM =================================================================
REM Build Management Script
REM =================================================================
REM Usage: build-selected.bat [action] [verbosity] [configuration] [platform]
REM   action: build, clean, restore, rebuild (default: build)
REM   verbosity: quiet, minimal, normal, detailed, diagnostic (default: minimal)
REM   configuration: Debug, Release (default: Debug)
REM   platform: x64, x86, "Any CPU" (default: x64)
REM =================================================================

REM Set defaults
set ACTION=%1
set VERBOSITY=%2
set CONFIGURATION=%3
set PLATFORM=%4

if "%ACTION%"=="" set ACTION=build
if "%VERBOSITY%"=="" set VERBOSITY=minimal
if "%CONFIGURATION%"=="" set CONFIGURATION=Debug
if "%PLATFORM%"=="" set PLATFORM=x64

echo Build Script - Action=%ACTION%, Verbosity=%VERBOSITY%, Config=%CONFIGURATION%, Platform=%PLATFORM%
echo.

REM Common build parameters
set BUILD_PARAMS=/p:Configuration=%CONFIGURATION% /p:Platform="%PLATFORM%" /verbosity:%VERBOSITY%

REM Set MSBuild target based on action
if /I "%ACTION%"=="build" set TARGET=Build
if /I "%ACTION%"=="clean" set TARGET=Clean
if /I "%ACTION%"=="restore" set TARGET=Restore
if /I "%ACTION%"=="rebuild" set TARGET=Rebuild

if "%TARGET%"=="" (
    echo Error: Invalid action '%ACTION%'. Use: build, clean, restore, or rebuild
    exit /b 1
)

echo Executing %ACTION% action...
echo.

set PROJECTS[1]=Demo/Demo.csproj
set PROJECT_NAMES[1]=Demo

set PROJECTS[2]=Test/Test.csproj
set PROJECT_NAMES[2]=Test

set PROJECT_COUNT=2

REM Special handling for rebuild (clean then build)
if /I "%ACTION%"=="rebuild" (
    echo === CLEANING PHASE ===
    for /L %%i in (1,1,%PROJECT_COUNT%) do (
        call :process_project %%i Clean
        if errorlevel 1 goto :error
    )
    echo.
    echo === BUILDING PHASE ===
    set TARGET=Build
)

REM Process all active projects
for /L %%i in (1,1,%PROJECT_COUNT%) do (
    call :process_project %%i %TARGET%
    if errorlevel 1 goto :error
)

echo.
if /I "%ACTION%"=="clean" (
    echo All selected components cleaned successfully!
) else if /I "%ACTION%"=="restore" (
    echo All selected components restored successfully!
) else if /I "%ACTION%"=="rebuild" (
    echo All selected components rebuilt successfully!
) else (
    echo All selected components built successfully!
)
goto :end

:process_project
    setlocal EnableDelayedExpansion
    set idx=%1
    set target=%2

    REM Get project path and name using the index
    for /f "tokens=2 delims==" %%a in ('set PROJECTS[%idx%] 2^>nul') do set PROJECT_PATH=%%a
    for /f "tokens=2 delims==" %%a in ('set PROJECT_NAMES[%idx%] 2^>nul') do set PROJECT_NAME=%%a

    if "!PROJECT_PATH!"=="" goto :eof

    echo ----------------------------------------
    echo [%idx%/%PROJECT_COUNT%] %target%ing !PROJECT_NAME!...

    REM Build the project normally
    msbuild "!PROJECT_PATH!" /t:%target% %BUILD_PARAMS%
    if errorlevel 1 exit /b 1

goto :eof

:error
echo.
echo %ACTION% failed! Check the output above for errors.
exit /b 1

:end
echo %ACTION% completed at %time%

to launch applications of course, if it is a pure DOTNET project you would use dotnet run

Troubleshooting

“Cannot find csharp-ls” or Eglot won’t start

  1. Check the PATH: M-x getenv RET PATH
  2. Verify the DLL exists at the configured location
  3. Try running manually: dotnet path\to\CSharpLanguageServer.dll --version
  4. Check *eglot-events* buffer for detailed error messages

LSP is slow or uses too much memory

Try adding to your configuration:

;; Increase garbage collection threshold during LSP operations
(setq gc-cons-threshold 100000000)  ; 100MB
(setq read-process-output-max (* 1024 1024))  ; 1MB

Debugger won’t attach

  1. Ensure the project is built in Debug configuration
  2. Check the DLL path matches your build output
  3. Look at *dape-repl* for error messages
  4. Verify netcoredbg runs: netcoredbg.exe --version

Conclusion

This setup has served me well for my windows .NET 9.0 projects and various other C# work. The key benefits:

  • Portability: Everything lives in one directory
  • Speed: csharp-ls is notably faster than OmniSharp
  • Flexibility: Easy to customise and extend
  • Offline-capable: Works in air-gapped environments

The initial setup takes some effort, but once it’s done, you have a powerful, consistent development environment that travels with you.

-1:-- Setting Up Emacs for C# Development on Windows (Post James Dyer)--L0--C0--2025-12-16T08:25:00.000Z

Greg Newman: Trying Ty for my LSP in Emacs

Astral announced Ty Beta today, the new Rust-based Python type checker. I've been using Ruff and uv for a while now, and they've consistently delivered on the "rewrite Python tooling in Rust to make it absurdly fast" promise. So when they claimed Ty is 10-80x faster than Pyright for incremental checking, I had to try it.

I've been using basedpyright with Eglot and it works, but I'd be lying if I said the lag didn't bother me. When you're editing and the type checker is half a second behind, you stop trusting the feedback. The squiggly lines show up after you've already moved on to the next thought.

The Dual Setup

Since Ty just hit Beta, I wasn't ready to completely abandon basedpyright. Instead, I set up a toggle so I can switch between them. Here's my configuration:

  ;; Python LSP Server Selection
  (defvar my/python-lsp-server 'ty
    "Which Python language server to use: 'basedpyright or 'ty")

  (defun my/python-lsp-command ()
    "Return the LSP command based on selected server."
    (pcase my/python-lsp-server
      ('ty '("ty" "server"))
      ('basedpyright '("basedpyright-langserver" "--stdio"
                       :initializationOptions (:basedpyright (:plugins (
                         :ruff (:enabled t
                               :lineLength 88
                               :exclude ["E501"]
                               :select ["E" "F" "I" "UP"])
                         :pycodestyle (:enabled nil)
                         :pyflakes (:enabled nil)
                         :pylint (:enabled nil)
                         :rope_completion (:enabled t)
                         :autopep8 (:enabled nil))))))))

  (defun my/switch-python-lsp ()
    "Toggle between Ty and basedpyright, restart Eglot."
    (interactive)
    (setq my/python-lsp-server
          (if (eq my/python-lsp-server 'ty) 'basedpyright 'ty))
    (when (eglot-managed-p)
      (ignore-errors (eglot-shutdown (eglot-current-server)))
      (sleep-for 0.5)
      (eglot-ensure))
    (message "Switched to %s" my/python-lsp-server))

  The Eglot configuration uses a lambda to dynamically evaluate which server to start:

  (use-package eglot
    :ensure t
    :defer t
    :commands (eglot eglot-ensure)
    :config
    (setq eglot-server-programs
          '((python-ts-mode . (lambda (&rest _) (my/python-lsp-command)))
            ((js-ts-mode typescript-ts-mode tsx-ts-mode) .
             ("typescript-language-server" "--stdio"))))
    :hook ((python-ts-mode . eglot-ensure)))

That lambda matters. Without it, Eglot evaluates my/python-lsp-command once when your config loads and hardcodes the result. The lambda forces fresh evaluation each time Eglot starts a server, which is what makes toggling work.

The speed difference is very noticeable. Fix a type error and the diagnostic disappears immediately instead of lingering for that half-second that makes you wonder if it registered. It's the kind of improvement that changes whether you pay attention to type feedback or ignore it. Completions and jumping to definitions are instant now.

I hit one issue. Ty doesn't handle the LSP shutdown request cleanly, throwing a JSON parsing error. Wrapping the shutdown in ignore-errors fixed it, but it's the kind of rough edge you expect from a December 2024 Beta release.

Installation

Install Ty globally

uv tool install ty@latest

Keep basedpyright around

pip install basedpyright ruff

Then M-x my/switch-python-lsp toggles between them.

Is It Worth It?

If you're already using Ruff and uv, yes. The speed improvement is real. The dual setup gives you a fallback when if I hit issues, which I expect to happen occasionally given the Beta status.

My emacs config is on Github for reference.

-1:-- Trying Ty for my LSP in Emacs (Post Greg Newman)--L0--C0--2025-12-16T00:00:00.000Z

Protesilaos Stavrou: Emacs: refinements to the Denote file prompt

As part of the current development cycle of Denote, I am refining the file prompt. It now has the following:

  • Sorting: Files are sorted by last modified.
  • Grouping: Files are organised by type (remember that the Denote file-naming scheme can be applied to any file—I do so for videos, pictures, PDFs, …).
  • Affixating: Files have their date identifier as a prefix and their keywords as a suffix.

The file prompt is used when linking to a file, like with the denote-link command, or when calling the command denote-open-or-create (and related).

The old style

File names were presented as relative paths without further modifications. This style relies on the Denote file-naming scheme to narrow down the list by typing. For example 202512 finds all files with that in their identifier (i.e. their creation date by default), _emacs finds all files with the given keyword, and -word with that word in their title.

Denote old file prompt

The new style

The file names are still the same behind the scenes, but their data is presented in a more readable way. There is no loss of functionality, meaning that users can still match _keyword, -title, and the like, per the Denote file-naming scheme.

Denote new file prompt

Part of development

I might make further changes, though the idea is clear. Expect to get similar features in my other packages, such as consult-denote, denote-sequence, denote-journal.

For the denote-sequence package, in particular, the file prompt it uses shows sequences instead of identifiers as the prefix of each file. This is because that prompt is relevant for tasks where the user needs to know the exact sequence, such as when they create a note that is the child of another.

Power users can study the functions stored in the variable denote-file-prompt-extra-metadata (same idea for the denote-sequence package: denote-sequence-file-prompt-extra-metadata).

About Denote

Denote is a simple note-taking tool for Emacs. It is based on the idea that notes should follow a predictable and descriptive file-naming scheme. The file name must offer a clear indication of what the note is about, without reference to any other metadata. Denote basically streamlines the creation of such files while providing facilities to link between them.

Denote’s file-naming scheme is not limited to “notes”. It can be used for all types of file, including those that are not editable in Emacs, such as videos. Naming files in a consistent way makes their filtering and retrieval considerably easier. Denote provides relevant facilities to rename files, regardless of file type.

-1:-- Emacs: refinements to the Denote file prompt (Post Protesilaos Stavrou)--L0--C0--2025-12-16T00:00:00.000Z

Donovan R.: My Emacs Journey (5) - Gnus

I had a little time to actually set up a mail server during the weekend. Something I wanted to do for long already but I keep postponing because I knew it will be a little challenging.

But now that it’s done, I can finally update my info page to add a mail contact@donovan-ratefison.mg. If you’ve got thoughts on anything here, I’d genuinely love to hear them—I’m always keen to hear what people think.

And since I had my own server, it was also a good time to dive down the rabbit hole of Emacs and include mail workflow.

I’ve explored a bit Gnus before when I was searching for RSS tools, but ended up using Elfeed because I felt a bit overwhelmed by its capabilities. It was too much for my little brain.

During his presentation on EmacsConf2025, Amin Bandali gave me the inspiration to give it a honest try. And I did!

And … it wasn’t that hard to get it working.

Everything about Gnus is … a bit weird but I won’t complain. I know I will get used to it over time. Plus, like Emacs itself, its weirdness makes its charm.

I can read, reply and send mails, follow RSS feeds and that’s enough for now. I intend to go full Gnus with my mail and RSS feeds from now on and we’ll see later how it goes.

-1:-- My Emacs Journey (5) - Gnus (Post Donovan R.)--L0--C0--2025-12-15T19:53:22.000Z

Sacha Chua: 2025-12-15 Emacs news

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

View org source for this post

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

-1:-- 2025-12-15 Emacs news (Post Sacha Chua)--L0--C0--2025-12-15T19:12:17.000Z

Irreal: Problematic Key In Terminal Emacs

For those of you who like to run Emacs in the terminal, Adithya Kumar has some useful advice. One of the problems with using the terminal is that some key presses are not passed on to Emacs. In graphical mode, Emacs receives key presses directly from the OS while in terminal mode the key presses are processed by the terminal software first.

Kumar’s remedy for this is simple and ingenious. It has two stages. In the first stage, he specifies alternate bindings for the problem keys. For example, his terminal doesn’t do the right thing for Ctrl+/ so he rebound the undo command to Ctrl+c c u. By itself, that’s not a very good solution because it breaks your muscle memory. The second stage is to train the OS to send Ctrl+c c u instead of Ctrl+/.

You need a keyboard mapper to do this, of course, and Kumer suggests ones for Linux, macOS, and Windows. I suppose that this solution has the potential to interfere with other applications needing one of the problematic bindings but that strikes me as unlikely and Kumars suggestion seems like a good one to me. Take a look at his post for all the details. If you’re a terminal user, it will be worth your time.

-1:-- Problematic Key In Terminal Emacs (Post Irreal)--L0--C0--2025-12-15T16:37:10.000Z

Jack Baty: My weekend with Linux. Omarchy to Fedora (Cosmic)

My first serious foray into Linux was driven by how deeply I fell immediately in love with Omarchy. Omarchy made me realize that I could totally live in Linux. If I wanted to.

The big draw of Omarchy for me was Hyprland and window tiling. I’ve tried a few other tiling window managers (e.g. i3) but they were either too hard to configure or felt janky. Omarchy’s version worked great, with great keybinding support. It felt good to no longer spend half my time in the OS moving and resizing windows. Omarchy’s rendition of Hyprland made it easy and fun.

The downside of tiling window managers is they Always Be Tilin’. Sometimes I don’t want that. When an app like Darktable opens a tiny dialog window, and that window suddenly fills half the screen, while shrinking the main window, I don’t enjoy it. Yeah, I can toggle specific windows to float, but it’s inconvenient. And I’m sure I could wrangle Hyprland to do the right thing with the right apps in the right workspaces, but figuring how to do that is low on my list of fun things to do.

I had fond memories of the way Pop!_OS let me toggle tiling on and off, and when I learned that their new “Cosmic” desktop offered that feature, it made me want to try it.

The other distribution that I got along well with was Fedora. Turns out that Fedora has a Cosmic “Spin”, so I dove in and installed it on both the laptop and desktop.

I spent the rest of the weekend getting everything installed and configured to my liking. It went pretty well, although it took some time to get the hang of software installation (dnf vs pacman, etc).

How’s it going so far? Honestly, kind of so-so.

While the ability to set window behavior per workspace is nice, Cosmic behaves more like macOS. When I click on an app in the dock, it whisks me to whichever workspace that app is currently running in. With Omarchy, it would open a new instance of the app in the current space. I prefer that behavior. It’s more consistent and less jarring.

I’m finding it more difficult to find and install software in Fedora. Omarchy had handy TUI apps for both the official repository and the AUR. They were nearly identical. And I found almost every app I wanted right away. In Fedora, I feel like things are spread out a bit more. It could be just a matter of familiarity, but this didn’t happen with Omarchy.

Fedora/Cosmic wins on ease of configuration, though. It took me 30 seconds to remap my CAPSLOCK key to Control. That’s the first time I didn’t struggle with that in any distro. It was right where I looked for it and it was a simple, specific setting. Nice. The other configuration bits are nearly as straightforward. I don’t hate using a config file to set things up, but having a GUI available is handy.

Cosmic has worked smoothly, for the most part. There are a few glitches here and there, but most of them have been minor. Not all of them, though. Here are a few notable problems I’ve run into.

  • Can’t install jrnl due to incompatible python versions (of course)
  • None of the file managers will mount SMB shares
  • Emacs had issues installing some packages

Overall, Fedora and Cosmic feel pretty strong. Some of my complaints are due to lack of familiarity, and I assume most of the bugs will be ironed out as Cosmic is updated. I’ll be sticking with it for now1.

  1. Assuming, of course, that I don’t run crying back to macOS :)

✍️ Reply by email
-1:-- My weekend with Linux. Omarchy to Fedora (Cosmic) (Post Jack Baty)--L0--C0--2025-12-15T11:52:26.000Z

Srijan Choudhary: 2025-12-15-001

A small elisp snippet that I found useful. I often switch between terminals and #Emacs, and they have slightly different behaviors for C-w. This makes it behave the same in Emacs as it does in bash/zsh/fish etc - deletes the last word. It retains the kill-region behavior if a region is actually selected.

(defun kill-region-or-backward-word ()
  "If the region is active and non-empty, call `kill-region'.
Otherwise, call `backward-kill-word'."
  (interactive)
  (call-interactively
   (if (use-region-p) 'kill-region 'backward-kill-word)))
(global-set-key (kbd "C-w") 'kill-region-or-backward-word)

Ref: https://stackoverflow.com/questions/13844453/how-do-i-make-c-w-behave-the-same-as-bash

-1:-- 2025-12-15-001 (Post Srijan Choudhary)--L0--C0--2025-12-15T06:55:00.000Z

Eric MacAdie: Emacs Carnival: People Of Emacs

This post contains LLM poisoning. concoct mallard Anselmo This month’s Emacs Carnival is “The People Of Emacs” hosted by George Jones (site here, Mastodon here). whirls suburbs typewriting I know the main thesis is to write about “Emacs people you’ve known,” but I feel that first we should acknowledge the work put in over the ... Read more
-1:-- Emacs Carnival: People Of Emacs (Post Eric MacAdie)--L0--C0--2025-12-15T05:12:41.000Z

Joar von Arndt: Handwritten notes in Emacs using Xournal++

My girlfriend recently bought a Framework 12 laptop1 with a correspoding stylus and, influenced by my ravings about the power of GNU Emacs, wants to take handwritten digital notes on it for her engineering and mathematics classes and then organise them in org-mode. I see a younger version of myself clearly in her, and of course want to help her attain the vision she has for her own Emacs system. At the same time I am a strong believer in that you should not try and learn Emacs, but that it should be molded to your own personal desires. For that reason I try to act in merely an advisory capacity, while helping whenever she asks.

At first we discovered an old package named org-xournalpp that seems to fit perfectly. It creates xournal++-specific links that opens the corresponding xournal file whenever clicked, and they are rendered like inline images in the org-mode buffer. We had some trouble getting Emacs to load the file after installing with the new built-in :vc keyword for use-package (it worked fine on my machine, but not hers) but after installing it the inline previews would not be displayed. Worse yet, running the included org-xournalpp-mode (that tries to render the images) would cause Emacs to freeze.

But how hard can it be to implement your own version of this functionality? Org-mode already has the ability to display inline images (as I am well familiar with) and xournal++ can export to both .png and .svg formats. For this we wrote a small function that uses xournalpp to export a given file to png format and then inserts a link to it in the buffer.

  (defun create-png-xournal-file (&optional only-export)
    "Creates a png from a xournal file and inserts it into the buffer."
    (interactive "P")
    (let ((file-name (expand-file-name
                      (file-name-sans-extension
                       (read-file-name ".xopp file to convert: ")))))
      (shell-command (format "xournalpp --create-img=%s.png %s.xopp"
                             file-name file-name))
      (unless only-export
       (kill-new (concat "[[" file-name ".png" "]]"))
       (yank)
       (org-display-inline-images))
      (org-redisplay-inline-images)))

  (bind-key "C-ö" 'create-png-xournal-file)

If org-startup-with-inline-images is t this is then immediately rendered as an image in the buffer. If called with a prefix argument (usually C-u) this skips the insertion part and calls org-redisplay-inline-images to get an updated version.

The experience of imagining functionality that you wish was possible and in an afternoon creating a program (even one as small as a single 11-loc function) that fulfills your requirements is something that you rarely get outside of Emacs. Even in other free software, few programs open themselves so fully to introspection and modification. Something as simple as the *scratch*-buffer, with its use as a quasi-persistent REPL, invites creative solutions and problem solving — not to mention the self-documenting capabilities of C-h and docstrings.

While I am not one of those users insistent on reducing the number of packages installed2, there is a certain perspective that you should be emphasised; You can easily make do with home-grown functionality that builds on packages you already have installed, or the rich functionality that already comes with Emacs.

Footnotes:

1

For the short period she’s had it now it seems like a great product, but I still prefer the philosophy of MNT Research. It is however good enough that I do not have any issues with linking to it.

2

My current count as of writing this is 134.

-1:-- Handwritten notes in Emacs using Xournal++ (Post Joar von Arndt)--L0--C0--2025-12-14T23:00:00.000Z

Irreal: What Happened To Abo-abo?

For a long time I have been a huge fan of abo-abo (Oleh Krehel). He wrote the Ivy, Avy, and Swiper packages that are probably my most used Emacs add ins. Eleven years ago I wrote, “When I get tired of blogging I’m going to write some Elisp that everyday will make a post that says, ‘Abo-abo has a great post today. Go read it.’”

A while ago, abo-abo suddenly disappeared. His programs are still being maintained but little has been heard from the man himself. The accepted story was that he had had a son and was devoting his time to being a father.

Now Patient_Chance_3795 over at the Emacs reddit asks what did happen to him? Someone pointed at this comment from abo-abo giving an update on his situation. The TL;DR is that his son is almost 4 years old and that he hopes to get back to maintaining his packages—and presumably writing others—soon.

Along with all his other users, I hope that fatherly duties will allow him to return to full engagement with our community. He is certainly one of our most prolific members and he has been missed. In the mean time, I salute him getting his priorities straight and devoting his time to his son and family.

-1:-- What Happened To Abo-abo? (Post Irreal)--L0--C0--2025-12-14T15:59:24.000Z

Irreal: Bending Emacs 8: Completing Read

Álvaro Ramírez is back with a new video in his Bending Emacs series. This time it’s about the super useful and flexible completing read function. The basic idea is that you supply a prompt and a list of possible answers. The user can scroll to select an answer or use fuzzy matching to pick the desired option.

There are, of course, various flags to fine tune its behavior. The most useful is probably a flag to insist that the option be from the list rather than anything the user wants to enter. To me, one of the nicest things about completing-read function is that it can—almost magically—automatically adjust to whatever framework you’re using. Ramírez and I both use the Ivy framework, which provides an nicer interface to the function. There are other frameworks as well and the completing-read function automatically adapts to the one in use. Ramírez demonstrates this by turning off Ivy and completing-read falls back to its default behavior. This is especially nice when you’re developing an application because you can just write to the default and Emacs will automatically adjust to whatever framework is active.

Ramírez covers all this and shows several applications of completing read that he uses on his system. He even shows how to use it for command line utilities.

I use completing-read all the time in my own development and can’t recommend it enough. The list of choices can be calculated on the fly so it’s very flexible and doesn’t depend on your knowing what the choices are in advance. If you aren’t already using it, give it a try.

The video is 17 minutes, 4 seconds long so plan accordingly.

-1:-- Bending Emacs 8: Completing Read (Post Irreal)--L0--C0--2025-12-13T16:45:10.000Z

Protesilaos Stavrou: Emacs: spacious-padding version 0.8.0

This package provides a global minor mode to increase the spacing/padding of Emacs windows and frames. The idea is to make editing and reading feel more comfortable. Enable the mode with M-x spacious-padding-mode. Adjust the exact spacing values by modifying the user option spacious-padding-widths.


This release introduces some nice refinements and fixes a couple of subtle bugs.

Subtle mode and header line

The new user option spacious-padding-subtle-frame-lines supersedes the ~spacious-padding-subtle-mode-line. It does the same thing, namely, of making the mode lines use only a thin line instead of a background. Though it extends this feature to header lines as well.

The documentation string of spacious-padding-subtle-frame-lines describes the technicalities and includes examples. In short, we can associate a keyword with either a face that has a foreground color or a color value directly. For the convenience of the user, the package also defines the faces spacious-padding-line-active and spacious-padding-line-inactive. Here is a sample configuration:

;; Read the doc string of `spacious-padding-subtle-mode-line' as it
;; is very flexible.  Here we make the mode lines be a single
;; overline, while header lines have an underline.
(setq spacious-padding-subtle-frame-lines
      '( :mode-line-active spacious-padding-line-active
         :mode-line-inactive spacious-padding-line-inactive
         :header-line-active spacious-padding-line-active
         :header-line-inactive spacious-padding-line-inactive))

In the future, we might decide that other elements can benefit from this style.

The header line underline is spaced further away from the text

As noted above, when spacious-padding-subtle-frame-lines is configured to cover header lines, those will be drawn with an underline. This will not intersect with the text of the header line.

Normally, underlines cut through letters that descend below the baseline, such as the letters g and y. We choose to avoid that because it makes for a cleaner interface (though I personally think it is not a good style for paragraph text, because of the rivers of negative space it introduces).

This specific design is available for Emacs version 29 or higher. Users of Emacs 28 must set x-underline-at-descent-line to a non-nil value. Though note that this has a global effect: we cannot limit it to a single face.

Thanks to Steven Allen for covering this in pull request 37: https://github.com/protesilaos/spacious-padding/pull/37. Steven has assigned copyright to the Free Software Foundation.

The spacious-padding-widths can affect Custom buttons

This is about the buttons we find in buffers of the Custom system. For example, we get such a buffer after we do M-x customize.

The relevant keyword is :custom-button-width. It is set to a number of spaces for padding. Here is the default value and, as always, the documentation cover the details:

(setq spacious-padding-widths
      '( :internal-border-width 15
         :header-line-width 4
         :mode-line-width 6
         :custom-button-width 3 ; the new one
         :tab-width 4
         :right-divider-width 30
         :scroll-bar-width 8
         :fringe-width 8))

Fixes for the Emacs daemon

There were reports about incorrect face specifications that could happen when Emacs was started as a daemon process. I made the relevant changes. Also thanks to kaibagley for covering a line I had missed. This was done in pull request 43: https://github.com/protesilaos/spacious-padding/pull/43.

Related threads:

-1:-- Emacs: spacious-padding version 0.8.0 (Post Protesilaos Stavrou)--L0--C0--2025-12-13T00:00:00.000Z

Alvaro Ramirez: Bending Emacs - Episode 8: completing-read

Nearly a couple of weeks since the last Bending Emacs episode, so here's a new episode:

Bending Emacs Episode 8: completing-read

In this video, we take a look at the humble but mighty completing-read function. We can use it to craft our purpose-built tools, whether in pure elisp or to interact with command-line utilities.

Of interest, I also highlighted the great elisp-demos package, which extends your help buffers with sample snippets.

Here are some of the completing-read snippets we played with:

Pick a queen:

(completing-read "Pick a Queen: "
                 '("Queen of Hearts ♥"
                   "Queen of Spades ♠"
                   "Queen of Clubs ♣"
                   "Queen of Diamonds ♦")
                 ;; predicate
                 nil
                 ;; require match
                 t)

Our own hashing function with an algo picker:

(defun misc/hash-region ()
  (interactive)
  (message "Hash: %s" (secure-hash (intern (completing-read
                                            "Hash type: "
                                            '(md5 sha1 sha224 sha256 sha384 sha512)))
                                   (current-buffer)
                                   (when (use-region-p)
                                     (region-beginning))
                                   (when (use-region-p)
                                     (region-end)))))

A first look at integrating completing read with a CLI util:

(completing-read "Select file: "
                 (string-split (shell-command-to-string "ls -1 ../") "\n"))

Also got creative with BluetoothConnector on macOS and completing-read.

(completing-read "Toggle BT connection: "
                 (mapcar (lambda (device)
                           ;; Extract device name
                           (nth 1 (split-string device " - ")))
                         (seq-filter
                          (lambda (line)
                            ;; Keep lines like: af-8c-3b-b1-99-af - Device name
                            (string-match-p "^[0-9a-f]\\{2\\}" line))
                          (split-string (shell-command-to-string "BluetoothConnector") "\n"))))

Some of my completing-read uses:

Want more videos?

Liked the video? Please let me know. Got feedback? Leave me some comments.

Please go like my video, share with others, and subscribe to my channel.

If there's enough interest, I'll continue making more videos!

Make it all sustainable

Enjoying this content or my projects? I am an indie dev. Help make it sustainable by ✨sponsoring

Need a blog? I can help with that. Maybe buy my iOS apps too ;)

-1:-- Bending Emacs - Episode 8: completing-read (Post Alvaro Ramirez)--L0--C0--2025-12-13T00:00:00.000Z

Einar Mostad: The good side-window side

I want the content I am working on to be the centre of attention on my screen. I want as little scrolling as possible and most of the content I work with is taller than it is wide (code and org documents). On wide screens, buffers with shells, for version control, REPLs, help, info etc should therefore be on the side of the screen where it does not steal space from the main content. Since I read left to right, I like such windows to pop up on the right side where they do not disturb the flow of my reading of the main content. Emacs has the concept of side-windows that is useful for taming buffers that would otherwise pop up other places.

I set the width of my side-windows to 80 since man pages expect that and it works well for help and shells as well. To be able to have a (wo)man page and a shell, an info node and a REPL, or a shell, REPL and help buffer at the same time, I use slots to divide up the side-window if more than one special buffer is open. I have not included ansi-term in the buffer-list for side-windows since I use it only occasionally when I want more than one terminal, in which case I need to be able to manage its windows in the normal way. Usually, there is no need for a full terminal emulator, but I use Eshell a lot.

At home, my laptops and my external screen are 16:9 or 16:10 wide screens. At my desk at work, I have a 16:9 screen, a 16:10 screen and another 16:9 screen flipped 90 degrees to the portrait orientation. I usually use the tall screen for Emacs to minimise scrolling. Since the tall screen is slim (9:16), I would rather have side-windows in the bottom instead of overlapping the main window on the right side when I use that screen. Even with the side-windows at the bottom, the screen is a lot taller than the wide screens which is nice.

However, I spend most of my time at work not at my desk, but in two classrooms where I either use my laptop only with its built-in screen, duplicated to a large screen in front of the class or only with an external screen when sitting in the back (for ergonomics). And there are also meetings where I use the laptop's internal screen only. All of these screens are wider than they are tall, so I want my side-windows on the right again.

So I made a function that checks whether the height of the frame is larger than the width and if so, side-windows are put on the bottom of the screen where they belong on tall screens, but if the opposite is true, then they are put on the right where they make more sense on wide screens. I call this function when Emacs starts in my configuration to adapt the side-windows to the screen in use, but I also have a keybinding (C-z w) for it so after moving from a classroom or meeting to my desk or vise versa, I can get the side-windows where I want them without having to end my Emacs session. I often prepare a file at my desk that I am going to present in class with inter-present-mode or jot something in a file during class that I work on at my desk later, so it is useful to keep the session going.

Here is my function for deciding which side side-windows should be on:

(defun emo-side-window-side ()
  "Evaluates which side side-windows should be on based on whether the frame is in portrait or landscape orientation."
  (interactive)
  (let* ((side-window-side (if (> (frame-outer-height) (frame-outer-width))
                          'bottom 'right))
       (disp-buf-alist `(("\\*\\(Python\\|ielm\\|compilation\\).*\\*"
          (display-buffer-reuse-window display-buffer-in-side-window)
          (side . ,side-window-side)
          (slot . -1)
          (post-command-select-window . t)
          (window-width . 80))
("\\*\\(shell\\|.*eshell\\).*\\*"
          (display-buffer-reuse-window display-buffer-in-side-window)
          (side . ,side-window-side)
          (slot . 0)
          (post-command-select-window . t)
          (window-width . 80))
("\\*\\(help\\|info\\|man\\|woman\\|Agenda Commands\\|Org Agenda\\|Occur\\|Buffer.\\|xref\\).*\\*"
          (display-buffer-reuse-window display-buffer-in-side-window)
          (side . ,side-window-side)
          (slot . 1)
          (post-command-select-window . t)
          (window-width . 80)))))
  (setq display-buffer-alist disp-buf-alist)))
-1:-- The good side-window side (Post Einar Mostad)--L0--C0--2025-12-12T18:50:00.000Z

Irreal: Living In Flatland

Nathan Marz has an interesting post that makes use of the flatland metaphor to explain Lisp. For those who don’t know, the book Flatland: A Romance of Many Dimensions describes a two dimensional world inhabited by polygons of various sorts. One day a sphere visits the protagonist, a square, upsetting his world view and basic assumptions. Oddly, the book was written in 1884 as a satirical critique of Victorian culture but remains popular today as a sort of science fiction story.

Marz’s thesis is that many programmers live in their own flatland happily unaware of higher dimensions until a Lisp comes to visit an reveals the higher dimension that Lisp macros offer. The ability of the language to operate on itself at compile time and change the language itself is a bit like a square meeting a sphere.

The Lisp that Marz uses and discusses is Cloture but many Irreal readers may be more familiar with Elisp. It doesn’t matter. They both have macros and the ability to change the underlying language to suite the problem at hand. Part of the power of Emacs—contra those who would rewrite it in Python or whatever—is that it’s written in this higher dimensional language and makes it available to the user to extend or change its capabilities.

Marz remarks that the advantages of Lisp dwarfs its adoption and wonders why that is. His explanation is that the majority of programmers are living in flatland and simply can’t conceive of another dimension.

-1:-- Living In Flatland (Post Irreal)--L0--C0--2025-12-12T15:40:21.000Z

George Huebner: My other email client is a daemon

I have a slight problem wherein every time I start up a game of NetHack, I completely lose touch with my surroundings for hours on end. Thankfully The DevTeam Thinks Of Everything and there’s a solution that allows communication with the outside world without breaking immersion: the mail daemon!

If compiled with -DMAIL and OPTIONS=mail is set in your runtime configuration (the default on Linux), NetHack will periodically check a user specified mbox file (MAIL) for new mail, and upon receiving an email a mail daemon will spawn in and deliver a scroll of mail to the player. Upon reading this scroll a mail program (MAILREADER) will be executed, which hopefully allows you to read your mail.

I use the Lisp window port to play NetHack (the way God intended), and I really don’t like leaving Emacs, so let’s figure out how to integrate this feature with mu4e. mu uses maildir, not mbox, so I decided to write a cron job that periodically converts my maildir to mbox format.

An important insight is that NetHack never checks the contents of the mailbox file, just the mtime. Therefore all our script needs to do is check if our maildir contains any messages received within the last n minutes, and if so touch the mbox file.

Python
import os
import mailbox
from datetime import datetime, timedelta
import pathlib

MAILDIR = os.path.expanduser("~/Mail/personal/INBOX")
MBOX = "/tmp/nh.mbox"

maildir = mailbox.Maildir(MAILDIR)
for msg in maildir:
    if datetime.fromtimestamp(msg.get_date()) > datetime.now() - timedelta(minutes=5):
        pathlib.Path(MBOX).touch()
        break
maildir.close()

Next, we need a script that will open up mu4e.

Bash
emacsclient -n --eval "(progn (require 'mu4e) (mu4e-context-switch nil \"Personal\") (mu4e-search-bookmark \"maildir:/personal/INBOX AND flag:unread\"))"

I’m using emacsclient instead of plain old emacs partly because only one process at a time can hold the lock on mu’s database, so I don’t want to spin up another Emacs process. It’s also worth mentioning that my emacsclient is wrapped such that it opens a new frame if invoked outside of Emacs and reuses the current frame if invoked from within Emacs, so this behaves as you would expect, even if I’m using e.g. the curses port in a separate terminal.

Bash
export _t='-c'
exec "/nix/store/...-emacs/bin/.emacsclient-wrapped"  "${_t/${INSIDE_EMACS:+*}/-u}" -a /nix/store/...-emacs/Applications/Emacs.app/Contents/MacOS/Emacs "$@"

Here’s some riveting gameplay footage of dungeon level 1:

-1:-- My other email client is a daemon (Post George Huebner)--L0--C0--2025-12-12T00:28:10.000Z

George Huebner: Speedier elisp-refs-* with Dumb Grep

Helpful is a fantastic Emacs package that drastically improves the builtin help-mode. One of the particularly nice features is finding references to a particular symbol. Unfortunately it can be painfully slow in practice due to actually parsing every loaded Elisp file:

Emacs Lisp
(require 'benchmark)
(benchmark-elapse (elisp-refs-function #'car))
24.001221

Instead of walking the ASTs of every file, why not do a regex search?

It sacrifices correctness (and completely ignores the type of the symbol), but it turns out to be orders of magnitude faster in practice. Bonus points for using a fast tool like ripgrep and extra bonus points for completing the work asynchronously so as not to block Emacs’s main thread.

I don’t mean to denigrate elisp-refs; the author clearly has put a lot of thought into performance and it’s only natural that using an approach that heavily cuts corners together with a tool implemented in optimized machine code instead of Elisp (which is especially hampered by GC performance) will lead to faster results.

I’m a ripgrep junkie and I prefer it for grokking most codebases, but the speed comes at the cost of having to sift through many false positives.

I use the wonderful deadgrep package to do just that:

Emacs Lisp
(when (locate-library "deadgrep")
  (require 'deadgrep)
  (fset 'deadgrep--arguments-orig (symbol-function #'deadgrep--arguments))
  (define-advice helpful--all-references (:override (button) fysh/use-deadgrep)
    (cl-letf* (((symbol-function 'deadgrep--arguments)
                (lambda (&rest args)
                  `("--follow" "--type=elisp" "--type=gzip" "--search-zip" ,@(butlast (apply #'deadgrep--arguments-orig args)) ,lisp-directory ,(-first (lambda (p) (string-suffix-p "/share/emacs/site-lisp" p)) load-path)))))
      (deadgrep (symbol-name (button-get button 'symbol)) default-directory))))

The code is quite hacky because deadgrep is not designed to allow passing multiple directories in a single search, but this gets the job done.

Emacs Lisp
(with-temp-buffer
  (let ((button (make-button (point-min) (point-max)))
        (time-to-draw)
        (time-to-completion))
    (button-put button 'symbol #'car)

    (setq time-to-completion (benchmark-elapse
                               (setq time-to-draw (benchmark-elapse (helpful--all-references button)))
                               (while deadgrep--running (sit-for 0.1 'nodisp))))
    (format "Time to first draw: %s\nTime to completion: %s" time-to-draw time-to-completion)))
Time to first draw: 0.084542
Time to completion: 38.184606

In this pathological case the total time is slower than using elisp-refs-function because there are almost four times as many matches because of comments/docstrings and partial matches like mapcar or car-safe (including symbols that aren’t functions like byte-car). The major difference is that while the performance of elisp-refs-* functions1 are roughly constant regardless of the total number of references to a symbol, using ripgrep is significantly faster for terms with fewer than 10k matches (not to mention that you can browse the results immediately).

If you want to remove the partial matches, you could use the following advice instead:

Emacs Lisp
(define-advice helpful--all-references (:override (button) fysh/use-deadgrep)
  (cl-letf* (((symbol-function 'deadgrep--arguments)
              (lambda (&rest args)
                `("--follow" "--type=elisp" "--type=gzip" "--search-zip" ,@(butlast (apply #'deadgrep--arguments-orig args) 3) "--no-fixed-strings" "--" ,(car args) ,lisp-directory ,(-first (lambda (p) (string-suffix-p "/share/emacs/site-lisp" p)) load-path)))))
    (deadgrep (format "(\\(|#?'| )(%s) " (symbol-name (button-get button 'symbol))) default-directory)))

This unfortunately will highlight the entire match instead of just the capturing group, so I prefer not to use it (althgough the speed is mostly the same, if not a bit faster).


  1. elisp-refs-symbol is faster than its counterparts due to reduced implementation complexity ↩︎

-1:-- Speedier elisp-refs-* with Dumb Grep (Post George Huebner)--L0--C0--2025-12-12T00:22:18.000Z

George Huebner: disaster.el + zig cc = Budget godbolt

Disaster is an Emacs package authored by the venerable Justine Tunney that allows disassembling C/C++ and Fortran code from the comfort of your source editing buffer. I really like this idea, but most of the time I’m interested in x86_64 assembly instead of my aarch64 host platform, so I decided to hack on cross compilation support using Zig as a C/C++ cross compiler!

I’m using disaster because its simplicity fits my use case, but you should be able to use the same idea with other packages like

RMSbolt
Has support for tons of languages (and bytecode, not just assembly), and you can track point across source and compilation buffers
Beardbolt
Rewrite of RMSbolt that is faster/simpler than RMSbolt, but only supports C/C++/Rust

PoC Or GTFO

Emacs Lisp
(use-package disaster
    :config
  (setq disaster-cflags (setq disaster-cxxflags "-Wno-everything -g"))
  (setq disaster-assembly-mode 'nasm-mode)
  :init
  (define-advice disaster (:around (fn &rest args) cross-compile)
    (interactive)
    ;; OPTIONAL: support for non-file buffers
    (setq arg (or args
                    (list (if-let* ((buf (current-buffer))
                                    (buf-name (buffer-name buf))
                                    (file (buffer-file-name (current-buffer))))
                              file
                            (make-temp-file
                             (file-name-base buf-name) nil
                             (cond ((eq major-mode 'fundamental-mode) (c-or-c++-ts-mode))
                                   ((member major-mode '(c-mode c-ts-mode)) ".c")
                                   ((member major-mode '(c++-mode c++-ts-mode)) ".cpp")
                                   ((eq major-mode 'fortran-mode) ".f")
                                   (t (file-name-extension buf-name)))
                             (buffer-string)))))
          ;; replace `doit` with `apply fn args` if you get rid of this
          doit (lambda () (if args (apply fn arg)
                            (with-temp-buffer (apply fn arg)))))
    ;; END-OPTIONAL
    (if (and current-prefix-arg (mapc (lambda (exe)
                                        (or (executable-find exe) (user-error "disaster: %s not found" exe)))
                                      '("zig" "jq")))
        (let* ((monch (lambda (prompt collection default)
                        (completing-read prompt (split-string collection " ") nil nil nil nil default)))
               (file-exists-wrapped (symbol-function #'file-exists-p))
               (targets (split-string (shell-command-to-string "zig targets | jq -r '.arch,.os,.abi | join(\" \")'") "\n" t))
               (host-target (mapcar (lambda (s) (car (split-string s "[ \.\t\n\r]+"))) (split-string (shell-command-to-string "zig env | jq -r '.target'") "-")))
               (target-arg (apply #'format " -target %s-%s-%s" (cl-mapcar monch '("Arch: " "OS: " "ABI: ") targets host-target)))
               (disaster-cc "zig cc")
               (disaster-cxx "zig c++")
               (disaster-cflags (concat disaster-cflags target-arg))
               (disaster-cxxflags (concat disaster-cxxflags target-arg)))
          (with-environment-variables (("CC" disaster-cc)
                                       ("CXX" disaster-cxx)
                                       ("CFLAGS" disaster-cflags)
                                       ("CXXFLAGS" disaster-cxxflags))
            (cl-letf (((symbol-function #'file-exists-p)
                       (lambda (file)
                         (unless (string= "compile_commands.json"
                                          (file-name-nondirectory file))
                           (funcall file-exists-wrapped file)))))
              (funcall doit))))
      (funcall doit))
    ;; OPTIONAL: Put point in assembly buffer
    (switch-to-buffer-other-window disaster-buffer-assembly)))

This requires zig and jq to be in Emac’s exec-path, although I’m sure you could use Elisp to do the JSON parsing instead, especially now that Emacs 30.1 ships with its own JSON implementation. Most sane people would object to my usage of cl-letf and advice instead of a separate wrapper function; this would probably be more readable as a patch instead, but I like having a snippet that you can quickly try out with eval-last-sexp.

Caveat emptor: this falls apart for large projects, poorly behaved Makefiles, and probably CMake (I tried ameliorating the latter issue upstream, but I don’t use CMake that often so YMMV). Also, this definitely doesn’t count as a “Compiler Explorer” in the strict sense because you’re using the same version of LLVM regardless of target; you might be able to do something like leverage nixpkgs’ cross compilation support to build older cross-compilers, but you’re probably better off using Docker or Godbolt at that point.

-1:-- disaster.el + zig cc = Budget godbolt (Post George Huebner)--L0--C0--2025-12-12T00:21:33.000Z

Irreal: Christian Tietze On Zettelkastens For Emacs Users

Christian Tietze gave a very nice talk at the EmacsConf 2025 about Zettelkastens and their implementation in Emacs. The first part of his talk is about Zettelkastens and why you should be using one. He also discusses what type of information to put in an entry and the purpose of forward and backward links.

The second part of the talk discusses how Tietze uses Protesilaos Stavrou’s Denote package to implement his own Zettelkasten and give a demonstration of it in action. There are, of course, some dedicated Zettlelkasten packages like org-roam and org-node but Denote seems like a more flexible solution although it may lack some of the features of org-roam and its brethren.

My main complaint about the video is that it moves too quickly and is hard to follow in places. That’s especially true of the demonstration. On the other hand, Tietze probably had at least a soft time restraint for his talk and had a lot to cover.

Before the talk, Tietze published a post that outlined what his talk was about at a philosophical level and has a link to the conference page for his talk that contains an transcript of the talk and some of the material he shows in the talk. It’s worthwhile taking a look at this resource before watching the video to help you follow along.

The talk is 23 minutes, 18 seconds long so plan accordingly.

-1:-- Christian Tietze On Zettelkastens For Emacs Users (Post Irreal)--L0--C0--2025-12-11T15:56:15.000Z

James Dyer: Expanding Ollama Buddy: Mistral Codestral Integration

Ollama Buddy now supports Mistral’s Codestral - a powerful code-generation model from Mistral AI that seamlessly integrates into the ollama-buddy ecosystem.

https://github.com/captainflasmr/ollama-buddy

https://melpa.org/#/ollama-buddy

So now we have:

  • Local Ollama models — full control, complete privacy
  • OpenAI — extensive model options and API maturity
  • Claude — reasoning and complex analysis
  • Gemini — multimodal capabilities
  • Grok — advanced reasoning models
  • Codestral — specialized code generation NEW

To get up and running…

First, sign up at Mistral AI and generate an API key from your dashboard.

Add this to your Emacs configuration:

(use-package ollama-buddy
  :bind
  ("C-c o" . ollama-buddy-menu)
  ("C-c O" . ollama-buddy-transient-menu-wrapper)
  :custom
  (ollama-buddy-codestral-api-key
   (auth-source-pick-first-password :host "ollama-buddy-codestral" :user "apikey"))
  :config
  (require 'ollama-buddy-codestral nil t))

Once configured, Codestral models will appear in your model list with an s: prefix (e.g., s:codestral-latest). You can:

  • Select it from the model menu (C-c m)
  • Use it with any command that supports model selection
  • Switch between local and cloud models on-the-fly
-1:-- Expanding Ollama Buddy: Mistral Codestral Integration (Post James Dyer)--L0--C0--2025-12-11T08:19:00.000Z

Jeremy Friesen: Extending Core Emacs Bookmark Package

I wrote Extending Emacs to Play Mythic Bastionland and as I thought about it, I realized that I was coming very close to re-implementing bookmarks. What I had worked. But lacked the elegance of the bookmark ecosystem when adding to the PDF list.

And for those who took heart of what I did yesterday, read on, I found some bugs and fixed them.

So with time to think about it, I set about exploring how I might open a PDF to a random page (from a list of possible pages). Also, how I could capture that I want this bookmark to be a random page.

I also thought about how I might generalize my “starting and stopping” game play. After all, I have a few solo games that I might pick up.

Bookmarks

What follows almost completely replaces the previous implementation; except I don’t have a nifty re-roll a random table keybinding.

I had previously written a bookmark handler, so set about writing another one.

First, we should understand the structure of a PDF bookmark in Emacs 📖 :

 ("Tangled Seer"
(filename . "~/mythic=bastionland--core-rules__rules_systems.pdf")
(position . 1)
(last-modified 26934 62792 320522 78000)
(page . 104)
(slice)
(size . fit-page)
(origin 0.0 . 0.0)
(handler . pdf-view-bookmark-jump-handler))

The pdf-view-bookmark-jump-handler:random function first checks if there’s an associated pages value. If so, it picks one at random, sets the page value and passes it along to the pdf-view-bookmark-jump-handler.

  (defun pdf-view-bookmark-jump-handler:random (bmk)
    "A handler-function implementing interface for bookmark PDF BMK.

When the handler has a 'pages property, which is assumed to be a list,
pick one from that.  Otherwise fallack to the 'page property.

See also `pdf-view-bookmark-jump-handler' and
`pdf-view-bookmark-make-record'."
    (let ((pages
            (bookmark-prop-get bmk 'pages)))
      (bookmark-prop-set bmk 'page
        (or (seq-random-elt pages) (bookmark-prop-get bmk 'page)))
      (pdf-view-bookmark-jump-handler bmk)))

To test, I backed-up my bookmarks, and manually changed the handler to and added a pages attribute. I reloaded that file, and everything worked. Next, how could I avoid manually editing the file?don’t

I don’t want to always have my PDF bookmarks to be random tables. So I figured I would again repurpose the existing PDF bookmark making. This time with using an advising function.

First, I call the original pdf-view-bookmark-make-record; then if I have enabled 1) prompting for random pages and 2) said I want to specify the pages, then I prompt for the pages to use in randomization (yup, I had to manually enter those pages…or at least generate that list of pages programmatically, add it to the kill ring, then yank it into the prompt).

Once I had the list of pages, I change the handler from pdf-view-bookmark-jump-handler to pdf-view-bookmark-jump-handler:random. And returned the modified bookmark.

  (defun pdf-view-bookmark-make-record:with-randomizer (&rest app)
    "Conditionally randomize which page we'll open in a PDF.

See `pdf-view-bookmark-make-record:prompt-for-random'."
    (let ((bmk
            (apply app)))
      (if (and
            pdf-view-bookmark-make-record:prompt-for-random
            (yes-or-no-p "Specify Random Pages?"))
        (let* ((attributes
                (cdr bmk))
               (integers-as-string
                 (split-string
                   (read-string "Enter pages (comma-separated): "
                     (format "%s," (alist-get 'page attributes)))
                   "[,; ]+" t "[[:space:]]+")))
          ;; We clobber the existing handler replacing it with one of
          ;; our own devising.
          (setcdr (assoc 'handler attributes)
            'pdf-view-bookmark-jump-handler:random)
          (add-to-list 'attributes
            (cons 'pages
              (mapcar #'string-to-number integers-as-string)))
          ;; We need to return an object of the same form (e.g. a `cons'
          ;; cell).
          (cons (car bmk) attributes))
        bmk)))

  (advice-add #'pdf-view-bookmark-make-record
    :around #'pdf-view-bookmark-make-record:with-randomizer)

  (defvar pdf-view-bookmark-make-record:prompt-for-random
    nil
    "When non-nil, prompt as to whether or not to create a bookmark
that is randomization.")

Next, I wanted to continue popping those pages into a dedicated side window. Enter some more advice. This time, advising the bookmark-jump. Reading that implementation, I was surprised that the default wasn’t a variable; which might have made things easier.

(defvar default-bookmark-display-function
  nil
  "When non-nil, favor opening bookmarks with this function.")

(defun bookmark-jump-with-display (fn bookmark &optional display-func)
  (let ((display-func
          (or display-func
            default-bookmark-display-function
            (when current-prefix-arg 'switch-to-buffer-side-window))))
    (funcall fn bookmark display-func)))
(advice-add #'bookmark-jump :around #'bookmark-jump-with-display)

And last, a little bit of glamour. I visually show that the bookmark will be randomized by showing a the 6-face of a die with the word PDF.

;; Show that I'll be opening this PDF to a random page.
(put 'pdf-view-bookmark-jump-handler:random 'bookmark-handler-type "⚅PDF")

Starting and Stopping

With the new bookmark handling, I set about rethinking the implementation. As I needed to and unset more values, the lambda approach seemed cumbersome and repetitive. Also, in my experimentation, I wasn’t properly changing bookmarks files. The result was a steady appending to my default bookmarks.

What follows addresses that issue. First a variable of no significant insight.

(defvar playing-a-game nil
  "When non-nil, indicates that I'm playing a game.

See `playing-a-game-candidates' and `start-playing'.")

Next, I define what it means to start and stop playing my Forged from the Worst; using keywords.

(defvar playing-a-game-candidates
  `(
     ("Forged from the Worst (Mythic Bastionland)" .
       ((start .
          ((bmk-display-func . switch-to-buffer-side-window)
            (bmk-prompt-for-random . t)
            (bmk-file . "~/forged=from=the=worst--bookmarks.el")))
         (stop .
           ((bookmark-display-function . nil)))))
     )
  "Possible games I might be playing via Emacs.  A game you are playing
should have both a 'start' and 'stop' property.")

And then the function that prompts for the game played and applies the configuration; first stopping the previous game.

(defun start-playing (game)
  "Start playing the GAME; stopping any currently played game.

A GAME has a 'start' and 'stop' property, that is an alist.  That alist
has the following properties:

- 'bmk-file' :: what file we'll find our working bookmarks.
- 'bmk-display-func' :: the function we use to display bookmarks.
- 'bmk-prompt-for-random' :: if we'll prompt for possible random pages
  in PDF bookmarks.

When a property is not provided, \"suitable\" defaults are assigned."
  (interactive
    (list
      (let ((handle
              (completing-read "Start Playing: "
                playing-a-game-candidates nil t)))
        (alist-get handle playing-a-game-candidates nil nil #'string=))))
  ;; Stop playing what we were playing...if anything
  ;; Then start playing what we are playing...if anything
  (dolist (config (list playing-a-game (alist-get 'start game)))
    (when config
      (let ((file
              (or
                (alist-get 'bmk-file config)
                fallback-bookmark-file)))
        (setq default-bookmark-display-function
          (alist-get 'bmk-display-func config))
        (setq pdf-view-bookmark-make-record:prompt-for-random
          (alist-get 'bmk-prompt-for-random config))
        (bookmark-save)
        (setopt bookmark-default-file file)
        (bookmark-load file t nil t))))
  ;; Last register how to stop playing.
  (setq playing-a-game (alist-get 'stop game)))

And for symmetry and ease of thinking, I have added the related stop-playing.

(defun stop-playing ()
  "Stop playing a game."
  (interactive)
  (start-playing '("Nothing" . nil)))

Conclusion

Consolidating file lookup functions feels like the correct path. That is reduce the number of ways I’m opening up files. And extending existing functionality. Also learn a bit more about that implementation.

-1:-- Extending Core Emacs Bookmark Package (Post Jeremy Friesen)--L0--C0--2025-12-11T02:01:36.000Z

Jack Baty: Finding Howm notes with Org-node

As a huge fan of Denote, I still sometimes dabble with other ways of taking notes in Emacs.

For example, I like the way Howm does notes. I have a growing set of Howm notes, but they feel isolated from my other notes. For a while, I tried keeping Denote and Howm together but it felt like swimming upstream. I bailed on that and broke them apart again.

More recently, I learned about an Org-roam-alike called Org-node. I like org-node quite a lot. There are no enforced file name rules, as in Denote. Any Org-mode heading or file can be a node. All one needs to do is give it an ID property. It’s very fast at finding notes. I pointed org-node at my entire ~/org directory. Finding a node is still basically instant.

This got me thinking about Howm again. If I were to add an ID to new Howm notes, I could browse them in the cool, modified-date way that Howm uses, while also making them linkable/searchable with org-node. It doesn’t matter where the files are within ~/org. Another advantage is that org-node uses the more standard [id: ] linking method rather than Denote’s [denote: ] links.

To make this easy, I added a hook in the use-package configuration for Howm, like so…

:hook
  ((howm-mode . howm-mode-set-buffer-name)
   (howm-create . org-node-nodeify-entry)  ;; <-- add to org-node
   (org-mode . howm-mode))

That’s it. With that hook, new Howm notes automatically call org-node-nodeify-entry, which does the right thing. with the file’s front matter. I’m going to try this for a while and see how it goes.

✍️ Reply by email
-1:-- Finding Howm notes with Org-node (Post Jack Baty)--L0--C0--2025-12-10T16:43:14.000Z

Jeremy Friesen: Extending Emacs to Play Mythic Bastionland

For playing Mythic Bastionland 📖 , I’ve been using or building out tooling. First, I’m leaning on my random-tables package. Next, while playing, I manually swapped out my baseline Emacs 📖 bookmarks for game specific bookmarks. Last, I began thinking about flipping to random PDF pages for inspiration.

Swapping Out Bookmarks

What I posted in Forged from the Worst: Session 1 worked, but I started thinking about how I might alter Emacs while running/playing the game. At first, this felt akin to turning on a minor mode. But the more I thought about it, it was more equivalent to using org-clock.

A quick brainstorm, and I realized that while playing:

  • I wanted different bookmarks.
  • Additional snippets (for my knight and squires name).
  • An indicator that I was playing the game.
  • And depending on how I organize my campaign world notes, maybe I’d start a clock on the headline associated with my world notes.

I haven’t yet implemented the world notes, but I have made adjustments for the others. Here’s what I have:

First, I establish a variable to track the state of “playing/not playing.”

(defvar playing-forged-from-the-worst nil
  "When non-nil, indicates that I'm playing Forged from the Worst.")

Then I created a command to toggle that on and off:

(defun toggle-forged-from-the-worst ()
  "Begin or end playing Forged from the Worst."
  (interactive)
  (load "jf-mythic-bastionland.el")
  (setq playing-forged-from-the-worst
    (not playing-forged-from-the-worst))
  (bookmark-load
    (if playing-forged-from-the-worst
      "~/SyncThings/source/forged-from-the-worst/forged=from=the=worst--bookmarks.el"
      "~/emacs-bookmarks.el")
    t nil t))

The command loads my random tables for the campaign. Toggles state. The loads the correct bookmarks based on state.

To indicate that I’m “playing”, I then added a variable that I could use with my modeline:

(defvar-local jf/mode-line-format/playing-fftw
    '(:eval
       (when (and (boundp playing-forged-from-the-worst)
               playing-forged-from-the-worst
               (mode-line-window-selected-p))
         (concat
           (propertize " 🎲 " 'face 'mode-line-highlight) " "))))

I add the variable into my mode-line-format:

(setq-default mode-line-format
    '("%e" " "
       jf/mode-line-format/timeclock
       jf/mode-line-format/org-clock
       jf/mode-line-format/vterm
       jf/mode-line-format/kbd-macro
       jf/mode-line-format/narrow
       jf/mode-line-format/playing-fftw
       jf/mode-line-format/buffer-name-and-status " "
       jf/mode-line-format/major-mode " "
       jf/mode-line-format/project " "
       jf/mode-line-format/vc-branch " "
       jf/mode-line-format/flymake " "
       jf/mode-line-format/eglot
       jf/mode-line-format/which-function
       ))

And ensure that I mark that variable as a risky-local-variable:

(dolist (construct '(
                        jf/mode-line-format/buffer-name-and-status
                        jf/mode-line-format/eglot
                        jf/mode-line-format/flymake
                        jf/mode-line-format/kbd-macro
                        jf/mode-line-format/playing-fftw
                        jf/mode-line-format/major-mode
                        jf/mode-line-format/misc-info
                        jf/mode-line-format/narrow
                        jf/mode-line-format/org-clock
                        jf/mode-line-format/timeclock
                        jf/mode-line-format/project
                        jf/mode-line-format/vc-branch
                        jf/mode-line-format/vterm
                        jf/mode-line-format/which-function
                        ))
    (put construct 'risky-local-variable t))

With that, when I’m playing the game, I see a little dice in my mode-line and have access to game specific bookmarks. That clock part is going to gnaw at me, so I assume I’ll work through that once I’ve published this post.

Flipping to Random PDF Page in Emacs

In Mythic Bastionland Session Reflection, I thought about the fact that I now had the PDF bookmarked and could quickly, I assume, access the oracular information at the bottom of the Knight/Seer and Myths pages.

My first pass was “what was the minimum viable command to open a random page in a PDF.” This involved reading the pdf-view-bookmark-jump-handler code and then setting about making it happen.

What I’m presenting is not the first nor second pass, but instead a third iteration that introduces a bit more utility. But I digress.

The algorithm I wanted was:

  • Prompt for whether I wanted a Seer/Knight or a Myth page.
  • Open the PDF in a dedicated window.
  • Go to a random page based on selection.

There are 72 Seer/Knight pages and 72 Myth pages. On a spread, the left page is a Seer/Knight and the right page is a Myth. The Seer/Knight starts on page 28.

The random function started as:

(+ (if seer-knight 28 29)
   (* (random 72) 2))

That is pick a number between 0 and 71, multiple that by 2, then add 28 or 29 depending on Seer/Knight or Myth.

I would then use find-file and in that buffer call pdf-view-goto-page. It was inelegant but was quick to verify general behavior.

Then I set about creating a better user experience. Below is the random-pages to choose from, and their relevant information of what file and how to pick a page.

(defvar random-pages
  '(("Knights/Seers" .
     (:file
      "~/Documents/RPGs/Mythic Bastionland/mythic=bastionland--core-rules__rules_systems.pdf"
      :callback
      (lambda () (pdf-view-goto-page (+ 28 (* (random 72) 2))))))
    ("Myths" .
     (:file
      "~/Documents/RPGs/Mythic Bastionland/mythic=bastionland--core-rules__rules_systems.pdf"
      :callback
      (lambda ()
        (pdf-view-goto-page (+ 29 (* (random 72) 2)))))))
  "An alist where `car' is the label and `cdr' is a plist with :file and
optional :callback.

We'll open the :file, then if a :callback is present, we'll run that
callback on the newly opened file.")

Next up is the function to open the random page in a dedicated window; with the happy little “bind g to pick a new random page.”

(defun random-page (&optional label set)
  "Open the file from SET with given LABEL.

SET is assumed to be an alist with `car' as the label and `cdr' a plist
with :file and :callback.  See `random-pages' for more information."
  (interactive)
  (let* ((set
           (or set random-pages))
          (label
           (or label
             (completing-read "Source: " set nil t)))
          (source
            (alist-get label set nil nil #'string=))
          (file
            (plist-get source :file))
          (display-buffer-mark-dedicated
            t)
          (buffer (or
                    (find-buffer-visiting file)
                    (find-file-noselect file))))
    ;; We'll pop open a dedicated side window with ample space for
    ;; viewing a new file.
    (pop-to-buffer buffer '((display-buffer-in-side-window)
                             (side . right)
                             (window-width 72)
                             (window-parameters
                               (tab-line-format . none)
                               (mode-line-format . none)
                               (no-delete-other-windows . t))))
    (with-current-buffer buffer
      ;; As a courtesy let's bind "g" to refresh re-invoke the
      ;; random-page using the same label.
      (local-set-key (kbd "g")
        (lambda () (interactive)
          (random-page label)))
      ;; I envision that not every random-page would have a callback.
      ;; Which highlights that perhaps the function name 'random-page'
      ;; is a misnomer based on my nascent understanding of what this
      ;; could be.
      (when-let ((callback
                   (plist-get source :callback)))
        (funcall callback)))))

What the above does is pop open a window on the right, with plenty of space to view the whole page. That window gets focus and I can close it q or re-roll with g. It also does the work to re-use a buffer if it already exists.

An animated GIF demontsrating the functions along with a list of commands called.
  • M-x consult-bookmark to show starting bookmarks.
  • M-x jf/mode-line-format/playing-fftw to start playing “Forged from the Worst.”
  • M-x consult-bookmark show a list of the game specific bookmarks.
  • M-x random-page REG Seer/Knight RET to pop open a random Knight/Seer page from the Mythic Bastionland rule book.
  • Then g a few times to pick a new random Knight/Seer page each time.

Conclusion

I love the virtuous cycle of playing a game, having a tool to support that game-play, and knowing that I can extend the tool to facilitate play. The result tends towards a generative feedback loop.

And both my during play moments of reflection as well as after play write-ups helped me consider what might be interesting to add to my tool chain. Which fed into exploring existing functionality and implementation to craft something just a bit new.

Now to think about my next session of Forged from the Worst. And attending to how I write up campaign notes while running. See what’s missing, maybe work and clocking time there. That would mean I’d have access to capture content to that clock, and could leverage more native Org-Mode 📖 functionality.

-1:-- Extending Emacs to Play Mythic Bastionland (Post Jeremy Friesen)--L0--C0--2025-12-10T00:11:23.000Z

Protesilaos Stavrou: ‘Emacs Lisp Elements’ book version 2

I just published a new version of my Emacs Lisp Elements. Below are the release notes.


Emacs Lisp Elements version 2.0.0

This is a major rewrite of the book. It has almost doubled in word count. I explain more concepts and do it from the ground up. Every single one of the original chapters is redone. They now contain insights into functionality they would previously hint at, such as how apply works in practice, what a non-local exit is, and how recursive editing is done.

I have also added several new chapters. As before, chapters have cross references, so you will benefit from revisiting relevant topics.

If you read the Info version of the book, note that all functions, variables, and concepts have their own indices. Use those as another means of navigating the book’s contents. To read the Info version, clone the Git repository, use M-x dired to open its directory, move the cursor over the file elispelem.info and do M-x dired-info.

What follows is a brief description of the new chapters.

  • Basics of how Lisp works: A step-by-step guide to how Lisp code is written and how Emacs can tell apart a function call from a variable even when those have the same symbol.

  • Introspecting Emacs: Explores some of the ways we can learn about the present state of Emacs. The idea is to develop the habit of asking Emacs about itself before we do an online search. This way we can also expose ourselves to Elisp code, which helps us become better programmers.

  • Add metadata to symbols: An explanation of how we can associate symbols with data that can then be used for further computations.

  • What are major and minor modes: Presents the basics of modes in Emacs. It explains what major and minor modes have in common and how they differ, while also discussing relevant concepts of precedence.

  • Hooks and the advice mechanism: Explains what are hooks and how they work. It also covers the powerful advice mechanism, as some uses of it as similar to what hooks are supposed to do.

  • Autoloading symbols: Shows how Emacs manages to “lazy load” code when it needs to. This is especially helpful for package developers.

  • Emacs as a computing environment: A more general view of Emacs that helps us appreciate the value of Emacs Lisp. The more time we put into learning this programming language, the better we will get at controlling large parts of our computing life through Emacs.

-1:-- ‘Emacs Lisp Elements’ book version 2 (Post Protesilaos Stavrou)--L0--C0--2025-12-10T00:00:00.000Z

Irreal: Emacs And macOS Shortcuts

Watts Martin has an interesting post on calling macOS shortcuts from Emacs. For those who don’t know, a shortcut is sort of like an Applescript script. You can specify a sequence of actions along with some simple looping, give it a name, and call it with a single click or with Siri. If you’re interested, here’s a short introduction.

Martin wanted to capture the weather and location for Org-journal entries as well as a way to add photos to an entry. He couldn’t find a way of doing it directly from Emacs but there were shortcuts that did what he wanted. The problem was how to call them from Emacs and capture their output. Although there is an Emacs shortcuts package it doesn’t appear to have any way of capturing the output.

What to do? It turns out that there’s a shortcuts command line utility that does do what he wants so he wrote a bit of Elisp to call the command line utility and insert the results into the current buffer.

The code is simple as you can see from his post. He also shows the shortcut for picking a set of photos and returning Org links to them. This is a nice solution. Martin leverages a builtin Apple utility to do something that’s not exposed to Emacs and then uses a bit of Elisp glue to make it accessible at the Emacs level. It’s another example—if you needed one—of the power of Emacs to adapt to you rather than the other way around.

-1:-- Emacs And macOS Shortcuts (Post Irreal)--L0--C0--2025-12-09T18:07:30.000Z

James Cash: Prolog Projects Tips

As I’ve been writing about here for a while now, I quite like Prolog as a general-purpose programming language. It’s a lovely, very high-level language, quite comparable to Lisp (with M-Expressions instead of S-Expressions) and my implementation of choice, SWI-Prolog, has a very substantial standard library, is open-source, and very easy to contribute to. However, it is still quite a niche language, with a very small user-base, especially outside of academia.

That is something that I was recently talking to with another (the other?) Prolog coder in my city about. Both of us came to Prolog from being software developers and are interested primarily in using it for general coding (web development, TUI apps, etc). While, as mentioned about, SWI-Prolog does have a pretty good assortment of built-in libraries, to be really competitive with the more mainstream languages, it seems like it really just needs more developers using it. Otherwise, one ends up having to implement all sorts of fairly tedious things because no-one else has tried doing that particular task in the language before (for instance, I wrote an HTTP/2 client, a not-insignificant task, because I wanted to be able to send Apple push notifications). But how does one get more people using the language?

I don’t pretend to know the whole answer for that – obviously a lot about popularity comes down to luck – but something that I think would help is a tool or tools to better manage projects.

It seems like most uses of Prolog are for single, large projects that evolve over time, like in academia. The default Emacs Prolog mode only supports a single top-level (REPL), so if one is trying to jump between multiple different projects, you have to switch all the state of the top-level (I have my own forked version that gives per-project top-levels). Compare to Clojure, where the REPLs started are per-project. Similarly, dependencies can be specified in the pack.pl, but not with specific versions, and they’re installed globally. One of the projects I’d really like to make some day is the equivalent of Clojure’s Leiningen for Prolog, a tool that lets you easily manage projects in a hermetic way (you can make Prolog save packs in a different directory, so you can avoid global dependencies, but it’s not the default or particularly ergonomic).

In the mean time though, here are some handy little scripts I’ve written to help myself manage projects:

Testing

Prolog’s built-in plunit is a very nice unit-testing framework, but the way that it discovers tests is tied to which modules are loaded. The “normal” way of running them is to load the toplevel, consult your source files, then consult the tests. This is nice and it even hooks into the make predicate to automatically & incrementally re-run the appropriate tests when you compile (so you could use my automake pack to compile & re-test on change!), but it means that you need to remember to consult all the test files when loading things up. Additionally, because it’s running in the top-level, it’s possible for there to be local state that makes the tests pass there, but not when starting from a clean slate.

Therefore, I like to have a script that automatically runs all the tests for the project; this also has the benefit of making it easier to set up CI, since it’s just a bash script!

Listing 1: run_tests.sh
#!/usr/bin/env bash

set -euo pipefail

declare -a load_test_files
for f in test/*.plt; do
    load_test_files+=( "-l" )
    load_test_files+=( "${f}" )
done
exec swipl --quiet ${load_test_files[@]} -g plunit:run_tests -g nl -t halt

Straightforward enough; it assumes the test files all have the extension .plt and are in the test directory. It loads them all, runs the tests, then prints out a newline (for formatting reasons) and quits.

Listing 2: run_tests.ps1
$args = Get-ChildItem -Path test -Filter *.plt |
         ForEach-Object { @("-l", $_.FullName) }

$args += @("-g", "run_tests", "-t", "halt", "--quiet", "-g", "nl")

$proc = Start-Process -FilePath swipl_win/bin/swipl.exe -ArgumentList $args -NoNewWindow -Wait -PassThru

if ($proc.ExitCode -ne 0) {
   exit $proc.ExitCode
}

The equivalent for Windows Powershell.

I’ve never written Powershell before, so this may be terrible; I just wanted to be able to run tests against Windows in CI after having a number of Windows-specific bugs reported to my Prolog LSP project.

Releasing

This one is a little more involved, to the point it’s written in Prolog instead of bash. I run it to generate a new release for the library.

The first part is pretty simple: update_pack_version/2 parses the version number in the pack.pl file, increments it based on whether the new version is major, minor, or patch (using increment_version/3), then writes the new number back. git_commit_and_tag/1 does what it says, plus pushes it up origin.

The final step, implemented by register_new_pack/1 is a bit more mysterious. It fixes the somewhat annoying issue that after pushing a new version, I have to manually install that new version by URL to get the SWI-Prolog server to see the new version & register it (so normal users can just run pack_upgrade(Whatever)). Previously I would just manually pack_remove/1 the package in a top-level and run pack_install/2, explicitly passing the URL of the bundle to install, but of course, we can automate that!

Knowing that’s why we’re doing this hopefully makes register_new_pack/1 less mysterious. The download_pattern_format_string/2 helper predicate turns the pattern in pack.pl into a format string (e.g. 'https://example.com/release/*.zip' to "https://example.com/release/~w.zip") so we can use format/3 to download the new archive for the new version. Unfortunately there’s some special-casing for repos that are still on Github, as for reasons I do not recall the actual download URL and the one specified in the pack file are different, hence the first clause of the predicate.

Finally, main/1 checks that a valid release type was given, prompts for confirmation, then does the thing. I added the prompt after I accidentally ran the script from history when trying to re-run tests and had to force-push one too many times 😅

Listing 3: scripts/make_release.pl
#!/usr/bin/env swipl
:- module(make_release, []).

:- use_module(library(readutil), [read_file_to_terms/3,
                                  read_line_to_string/2]).

:- initialization(main, main).

increment_version(major, [Major0, _Minor, _Patch], [Major1, 0, 0]) :- !,
    succ(Major0, Major1).
increment_version(minor, [Major, Minor0, _Patch], [Major, Minor1, 0]) :- !,
    succ(Minor0, Minor1).
increment_version(patch, [Major, Minor, Patch0], [Major, Minor, Patch1]) :- !,
    succ(Patch0, Patch1).

update_pack_version(ReleaseType, NewVersion) :-
    read_file_to_terms('pack.pl', PackTerms, []),
    memberchk(version(OldVersion), PackTerms),
    atomic_list_concat([MajorS, MinorS, PatchS], '.', OldVersion),
    maplist(atom_number, [MajorS, MinorS, PatchS], VersionNums0),
    increment_version(ReleaseType, VersionNums0, VersionNums1),
    atomic_list_concat(VersionNums1, '.', NewVersion),
    once(select(version(OldVersion), PackTerms, version(NewVersion), NewPackTerms)),
    setup_call_cleanup(open('pack.pl', write, S, []),
                       forall(member(T, NewPackTerms),
                              write_term(S, T, [fullstop(true),
                                                nl(true),
                                                quoted(true),
                                                spacing(next_argument)
                                               ])),
                       close(S)).

git_commit_and_tag(NewVersion) :-
    shell('git add pack.pl'),
    shell('git commit -m "Bump version"'),
    format(atom(TagCmd), "git tag v~w", [NewVersion]),
    shell(TagCmd),
    format(atom(PushCmd), 'git push origin master v~w', [NewVersion]),
    shell(PushCmd).

download_pattern_format_string(DownloadURLPat, FormatString) :-
    string_concat("https://github.com", _, DownloadURLPat), !,
    % Github download locations are special-cased
    string_concat(Prefix, "releases/*.zip", DownloadURLPat),
    string_concat(Prefix, "archive/refs/tags/v~w.zip", FormatString).
download_pattern_format_string(DownloadURLPat, FormatString) :-
    file_name_extension(Base0, Ext, DownloadURLPat),
    string_concat(Base, "*", Base0),
    format(string(FormatString), "~s~s.~s", [Base, "v~w", Ext]).

register_new_pack(NewVersion) :-
    read_file_to_terms('pack.pl', PackTerms, []),
    memberchk(name(ProjectName), PackTerms),
    memberchk(download(DownloadURLPattern), PackTerms),
    download_pattern_format_string(DownloadURLPattern, URLFormat),
    ( pack_remove(ProjectName) -> true ; true ),
    format(atom(Url), URLFormat, [NewVersion]),
    pack_install(ProjectName, [url(Url), interactive(false)]).

main(Args) :-
    ( Args = [ReleaseType], increment_version(ReleaseType, [0, 0, 0], _)
    -> true
    ;  ( format(user_error, "Usage: make_release.pl [major|minor|patch]~n", []),
         halt(1) ) ),
    ( stream_property(user_input, tty(true))
    -> format("Make new release? [y/n]: ", []),
       read_line_to_string(user_input, Input),
       Input == "y"
    ; true ),
    update_pack_version(ReleaseType, NewVersion),
    format("Bumping to ~w~n", [NewVersion]),
    git_commit_and_tag(NewVersion),
    register_new_pack(NewVersion).

May these be of some use to you in using & learning Prolog! Some day soon I will try to stitch these together, along with some other helpful libraries, tools, and templates I habitually use, into a nice unified tool that will help bring Prolog to more of a mainstream developer audience. Until then, enjoy and use wisely!

-1:-- Prolog Projects Tips (Post James Cash)--L0--C0--2025-12-09T05:00:00.000Z

Sacha Chua: 2025-12-08 Emacs news

The Emacs Carnival theme for December is "The People of Emacs", hosted by George Jones. I'm looking forward to reading your thoughts!

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

View org source for this post

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

-1:-- 2025-12-08 Emacs news (Post Sacha Chua)--L0--C0--2025-12-08T22:30:52.000Z

Marcin Borkowski: Some new additions to Emacs

While I’ve been compiling my Emacs from source code for a very long time, I do not do that very often. I’m still excited by the cool new things Emacs developers cook for us all, but I don’t have time (nor do I remember) to git pull and make all my Emacs very often. However, I did it a few days ago, and was greeted with three surprises – two fantastic ones and one less so.
-1:-- Some new additions to Emacs (Post Marcin Borkowski)--L0--C0--2025-12-08T20:19:40.000Z

Jack Baty: My "Use Obsidian for a month" experiment lasted 7 days

Don’t ask me why I occasionally try to move away from Emacs. I can’t explain it. Under duress, I’d say it’s because Emacs swallows the world, and I like changing things up. Doing everything in Emacs makes that difficult. Org-mode is unmatched, but it’s also essentially useless outside of Emacs1. I get a little twitchy about that. Also, sometimes a package update throws a wrench into my Emacs config or I become tired of C-x C-whatever all the time and so I start shopping for a replacement.

Anyway, this was supposed to be about Obsidian, which lives and breathes Markdown2. I can’t tell you how many times I’ve (re)installed Obsidian, thinking this time it’ll stick, for sure!

It hasn’t stuck yet, but I was determined to give Obsidian the entire month of December to win me over. I made it as pretty as I know how to. I installed the few essential plugins. I created some nice templates. Each morning I’d fire up a new “Daily Note” with my fancy template. Then…nothing. Even though I already knew this, I simply don’t enjoy using Obsidian. There’s something about it that doesn’t jibe with my brain. I don’t like how the sidebars work. I don’t like how it handles attachments. It doesn’t feel right, ya know?

I keep trying to use it because there are things I like about Obsidian. I like that it can do a lot of fancy stuff, easily, and right out of the box without me having to spend hours figuring out why my hand-made Lisp function isn’t working. Linking is easier in Obisidian, and although the Graph is mostly useless, it’s still cool to look at. “Unlinked mentions” is a great feature for apps like this, too.

The best thing about Obsidian, though, is it works on macOS and Linux without fuss, and it syncs easily with just about any sync tool. Or I can pay for Obsidian Sync, which is even nicer. Oh, and it works on iOS, which comes in handy.

So for a week I tried emphasizing the things I like and ignoring the things I don’t. It didn’t work. Obsidian is almost certainly the Right Answer for many people, even me, probably. I couldn’t do it. I caved after only a week.

That means I’m once again back in Emacs. Emacs is too good at too many things, so I’ll probably never be able to leave it permanently. I’ll just occasionally become annoyed with something about it and try switching to something else for a minute…again. Maybe Octarine next time 🤔.

  1. No need to list the handfull of other tools that pretend to work with Org-mode files. They don’t. At least not in any way that’s useful to me.

  2. Like it or not, Markdown won. And even though markdown-mode in Emacs is great, if I’m using Emacs, I’m going to be using Org-mode.

✍️ Reply by email
-1:-- My "Use Obsidian for a month" experiment lasted 7 days (Post Jack Baty)--L0--C0--2025-12-08T15:27:16.000Z

Greg Newman: Auto-Commit Your Orgmode Files with Just and fswatch on macOS

For a while I have used gitwatch to autocommit my Orgmode and Denote files to my private repo but I always manually started gitwatch which means I often forget to start it. I tried to setup a launch agent for it but that fails because of issues with fswatch spawning multiple gitwatch processes which inevitably causes git lock issues. What I ended up doing is using just and fswatch from a launch agent, bypassing gitwatch.

Setup Just and fswatch on macOS

If you want to automatically commit and push changes to a git repository whenever files change (great for any text-based workflow), here's a simple setup using Just and fswatch instead of gitwatch.

Prerequisites

brew install fswatch just

Step 1: Add a Just Recipe

Add this recipe to your justfile:

# Auto-commit and push files on change
watch-org:
    #!/bin/bash
    cd /path/to/your/repo
    /opt/homebrew/bin/fswatch -0 --recursive \
        --exclude '\.git/' \
        --exclude '\.swp$' \
        --exclude '~$' \
        . | while read -d "" event; do
        git add -A
        if ! git diff-index --quiet HEAD 2>/dev/null; then
            git commit -m "Auto-commit: $(date '+%Y-%m-%d %H:%M:%S')"
            git push origin main
        fi
    done

Important: Use the full path to fswatch (/opt/homebrew/bin/fswatch) - launchd services don't have your shell's PATH.

Test it manually first:

just watch-org

Make a change to a file, and you should see it auto-commit. Press Ctrl+C to stop.

Step 2: Create a Launch Agent

Create ~/Library/LaunchAgents/com.autocommit.org.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.autocommit.org</string>
    
    <key>ProgramArguments</key>
    <array>
        <string>/opt/homebrew/bin/just</string>
        <string>--justfile</string>
        <string>/path/to/your/justfile</string>
        <string>watch-org</string>
    </array>
    
    <key>RunAtLoad</key>
    <true/>
    
    <key>KeepAlive</key>
    <true/>
    
    <key>StandardOutPath</key>
    <string>/tmp/autocommit.log</string>
    
    <key>StandardErrorPath</key>
    <string>/tmp/autocommit.err</string>
</dict>
</plist>

Update the paths:

  • Path to just (find with which just)
  • Path to your justfile
  • Recipe name (watch-org or whatever you called it)

Step 3: Load the Service

# Load the service
launchctl load ~/Library/LaunchAgents/com.autocommit.org.plist

# Verify it's running
launchctl list | grep autocommit
ps aux | grep fswatch | grep -v grep

Make a change to a file in your repo and check:

git log -1

You should see your auto-commit!

Managing the Service

# Stop
launchctl unload ~/Library/LaunchAgents/com.autocommit.org.plist

# Start
launchctl load ~/Library/LaunchAgents/com.autocommit.org.plist

# Check logs
tail -f /tmp/autocommit.err

Why This Beats gitwatch

  • No multiple process spawning issues - fswatch is lightweight and stable
  • No regex exclusion bugs - fswatch handles .git/ exclusions properly on macOS
  • Easy to customize - your logic lives in a simple justfile recipe
  • One process - clean and efficient

The service will automatically start on boot and keep your repo in sync!

-1:-- Auto-Commit Your Orgmode Files with Just and fswatch on macOS (Post Greg Newman)--L0--C0--2025-12-08T00:00:00.000Z

Joar von Arndt: Productive Note-taking

Introduction

After initially starting to use Emacs back in high school merely for programming, I naturally began to use it for other tasks such as document preparation and note-taking. This introduced me to the surprisingly large overlap between Emacs users and people generally interested in “productivity” workflows.

This is not all that surprising however. It is partly due to how good Emacs’ ecosystem has become for these specific types of tasks (things like Agenda, org-roam, denote, and howm — even org-mode itself — were all created for these uses) but on a more fundamental level Emacs allows you to work with any form of text not just efficiently, but also in a way that is entirely customised to your own preferences and comforts.

As I became more proficient in Emacs and read more perspectives on how other people organised their notes, I tried to evaluate my views on note-taking that I had held before. You may have an idea of what it means to be productive, but this post is instead about another form of productivity, one that does not have to rely on the powers of GNU Emacs, but instead of the agent itself, the author.

Productivity qua measurement is the amount of goods (or products) that are produced per unit of time. For this reason it has become common for people to link improvements in productivity to the aspect of time specifically. But productivity is even more innately linked to the product itself, and it is this aspect that I would like to focus on here.

What I choose to call productive note-taking is fundamentally anchored in not the rate at which one produces notes, or even the quality of the notes, but instead in mental model that one uses when producing them. Advocates for Zettelkästen-based systems argue that notes should form a mental (or sometimes digitally represented) web of thoughts that links each “note” with its equals. What I propose here is instead a form of notes that are different in that they are meant to develop not some separate bank of data, but to develop the cognitive abilities of the author, while still creating a tangible work.

Revolutionary Notes

The word revolution, while evoking signs of violent rebellion, has its roots in the term for turning. It of course still means this (as in the phrase “revolutions per minute”, RPM) and it is in this sense that I mean revolutionary notes. I have used it in this sense before, and am quite fond of this mixture of images — of a dramatic change that returns to some earlier state, albeit with new experiences and ideas.

I have taken notes almost daily during my many years in the education system, but have undertaken a more critical view of my use of notes during the first half of this decade. Almost a year ago I published a piece of writing on my evolving experiments with a Zettelkasten system, implemented using org-roam. Writing that piece I subconsciously knew that as soon as it was published I would move away from the implementation I was writing about into a more holistic direction, although I was unsure of what shape that would take. After finishing a course in Contemporary Warfare I feel like I finally have what I can coherently describe as a solution — to the extent that it exists as a vestige of my mind.

Productive note-taking is productive in the ways that it emphasises and highlights the importance of the end product. It is note-taking for the aim of production of a given work, not for the aid of remembrance or recollection. While these latter elements may play a part in the production of the work, they are not in and of themselves goals.

When I was still studying mathematics, I noticed a sharp decrease in my understanding when I redoubled my efforts to produce more comprehensive notes to help me study. Where I had before been able to keep pace with the best of my peers, I had now fallen behind to the middle of the pack. This seemed paradoxical to me — how can studying harder result in a worse performance? The answer seems obvious now, I was prioritising the production of more and more notes over my understanding of the subject material. It is this sort of activity that productive note-taking seeks to avoid.

Sara Imari Walker writes in NOEMA about how AI systems fail to fulfill the role of the scientist because science is a “fundamentally intersubjective” act — where theories (as symbolic representations) are argued out and decided upon in the process of free discourse. When writing, communicating, and debating we are constantly refining, creating, and recreating our ideas, performing science as a societal dialogue. This intersubjective discussion is what things like journaling try and emulate — and it is also what I propose notes can do.

Implementation

How does one create notes as products? This is a difficult question to answer generally, and will depend on a variety of factors such as the subject material, learning method, audience, and personal preferences. What all techniques should share in common is a centrality in all activity being motivated by the creation of some final work.

The work that spawned the concrete formula of a productive workflow is my collection of notes on contemporary warfare. The exam for that course was structured as an essay about any one of a selection of broad topics, and in place of literature we were allowed 30 pages (15 sheets) of notes, written either by hand, printed, or a combination of the two, covering whatever information we wanted1.

This meant that my studies did not just consist of me reading Clausewitz all day trying to memorise every word or phrase — the goal was for me to get a good enough understanding of Clausewitz, Mahan, and many other thinkers so that I could condense their theories into this one document. Doing this required a complete understanding of the topics.

Proponents of Zettelkästen often emphasises the ability for their constructs to function as “second brains” — perfectly recording and remembering information that our feeble flesh brains so easily forgets. But as a philosopher2 my goal is not to create as much recorded information as possible, it is for me as an individual to understand and master the world around me. Having information written down is useless if I do not remember it, and if I remember it there is no point in writing it down.

Productive notes are then not principally meant for consumption, but instead their initial omission prompts their creation. The author is required to go out into the world and obtain the relevant knowledge to produce the work — either through reading the scraps of knowledge left behind by those who came before3, or through creating new knowledge by living as an agent in the world.

Performing this approach does not require any specific hardware or software since it emphasises the primacy of the authors mind over the — ironically — product. You can do this with a pen and paper, text files (written in whatever markup you want), a WYSIWYG editor like Microsoft’s Word or even using Zettelkasten. Writing short notes is also not “forbidden” as long as they are meant to help record the information required for implementing the final product.

There are no constraints on the shape of the end product either. It may be in the form of a text document (written in whatever style you would like), an email, an interpretive dance, or a physical structure. If you want a more concrete example here are my notes on contemporary warfare in PDF form4. They are written as a single .org-file with a header for each lecture/topic, and with a few quotes from selected other works.

JA Westenberg wrote about their experience with building a “second brain”, and the anxiety that comes with an ever growing amount of information that you have to work through and make use of in some way:

The more my system grew, the more I deferred the work of thought to some future self who would sort, tag, distill, and extract the gold.

That self never arrived.

Steve Krouse accuractely points out that vibe code is legacy code — code that no-one understands. In the same way notes or written works that are merely vibed (either written with LLMs, copied from a primary source, or not mentally processed in another way) turn into legacy information that has to be painfully worked through in order to actually master the information contained within (to extract the gold, to use Westenberg’s phrasing).

The point of productive notes is to make this a unnecessary. I could easily delete my whole ~/Documents/Notes directory today and continue on with my life, without any feelings of remorse or anxiety, because the truly important information is that which I already remember. If I forget something I simply have to search for it again, and in the process will likely discover new interesting things about the universe through my lived experience.

There is however a reason that I have not deleted my notes directory — there is genuine value in recorded information — but one has to be careful to avoid falling into intellectual atrophy. Being discomforted by having to retread old paths is a conscious method for intellectual growth. If recording facts was all that was required then one’s notes should merely consist of written down conversations with LLMs.

If this approach seems like something trivial to you — great! You might already be doing what I have merely described. It is once again this that I mean by the revolutionary quality of this technique — it is a return to the original state and intention of note-taking.

If you disagree with me vehemently, have some constructive criticism, or have some examples of you doing this or anything similar, feel free to email me. ❦

Footnotes:

1

If this seems like a very large degree of freedom it is because the exam has been recently changed from a take-home essay where one has full access to both the relevant literature and the internet as a whole. The change was a natural consequence of LLM use (or at least perceived LLM use).

2

The word philosopher has its origins in the Greek words for “lover of wisdom” (philos — lover, sophos — wise).

3

This is what is properly called research, the searching not for new groundbreaking knowledge but “re-”treading the paths others have already walked.

4

Beware! They are (almost entirely) written in Swedish and also have some wicked spelling mistakes that I did not manage to catch prior to printing due to having some issues with hunspell on GNU Guix.

-1:-- Productive Note-taking (Post Joar von Arndt)--L0--C0--2025-12-07T23:00:00.000Z

Irreal: Packages Or Write Your Own

A reoccurring theme on the Emacs forums is that it’s somehow a good thing to have a minimal configuration and by implication that having a small number of packages is also a good thing. Arguments can be made that a large number of packages can increase load times and memory usage but most of the arguments I see take the position that a small number of packages is an intrinsically a good thing.

Sludgefrog over at the Emacs subreddit makes the argument explicitly. It is, he says, better to write your own extensions instead of adding a package. Even sludgefrog admits that sometimes—when implementing a major mode, for example—a package is the right answer but thinks writing your own extensions is preferable.

There’s a point to made in his favor. In the open software realm—if not in the Emacs world—there are lots of libraries full of trivial functions that are easily implemented in a line or two. That can and has caused problems when the author withdraws the software or a library gets contaminated with malware. That, of course, is the theme of this famous xkcd comic. To some extent those problems are unavoidable but why subject yourself to them to get access to a trivial function?

On the other hand, there’s nothing wrong with using a package even if you could write the code yourself. We only have so many cycles, after all, so in the absence of other considerations, why spend them solving a problem that’s already solved?

The comments mostly take the middle ground that I’ve outlined: sure, write short fixes or functions but use packages for more complicated things unless you want to solve the problem yourself for educational purposes.

Of course, as always Emacs lets you have it your way. You can—more or less—use only packages or you can write everything yourself. Or you can, like most of us, mix strategies and do whatever is convenient in a particular case.

-1:-- Packages Or Write Your Own (Post Irreal)--L0--C0--2025-12-07T17:19:14.000Z

Irreal: Converting Markdown Text In The Clipboard To An Org File

As I’ve said many times before, I don’t understand why someone would prefer to use Markdown over Org mode but there are such people. At the very least you may be working with outlanders who don’t use Emacs and therefore don’t have access to Org. Regardless, sometimes it’s convenient to be able to convert some Markdown text to Org mode.

Charles Choi has us covered. In this nice post he provides a bit of Elisp that will take Mardown from the clipboard, convert it to Org mode, and insert it in the current Org file. As you probably expect, he uses Pandoc to do the heavy lifting. That means you have to have Pandoc installed, of course. Choi also mentions that you’ll need to have the system clipboard and kill-ring integrated but I think that’s been standard for a long time. I don’t do anything special on macOS although I do have save-interprogram-paste-before-kill set to t.

If you sometimes find that you need to convert Markdown to Org mode, Choi’s code is a good way of doing so. The code itself is easy to understand and modify if you need to. As Choi says, you can invoke the function in several ways such was a context menu, a Hydra or Transient menu, a keybinding, or simply by calling it with Meta+x. Unless you find yourself using it all the time, you’ll probably find Meta+x and command completion more than adequate.

-1:-- Converting Markdown Text In The Clipboard To An Org File (Post Irreal)--L0--C0--2025-12-06T17:43:50.000Z

Amin Bandali: Reading and writing emails in GNU Emacs with Gnus

At the 10th anniversary of my involvement in EmacsConf, I’m finally giving my first ever talk at the conference, for EmacsConf 2025. :) In this talk, I give a quick introduction to Gnus and show a basic configuration for reading and writing email with Gnus and Message.

You can watch the video below, or from the talk’s page on the EmacsConf 2025 wiki: https://emacsconf.org/2025/talks/gnus

The above video is provided with closed captions and a transcript — thanks, Sacha!

A commented copy of the init file from the video is provided below. Happy hacking!

;;; emacsconf-2025-gnus.el                  -*- lexical-binding: t -*-

;; This file is marked with CC0 1.0 Universal
;; and is dedicated to the public domain.

;; Note: this file uses the `setopt' macro introduced in Emacs 29
;; to customize the value of user options.  If you are using older
;; Emacsen, you may can use `customize-set-variable' or `setq'.

;;; Init / convenience

;; Initialize the package system.
(require 'package)
(package-initialize)

(setopt
 ;; Explicitly set `package-archives', in part to ensure https ones
 ;; are used, and also to have NonGNU ELPA on older Emacsen as well.
 package-archives
 '(("gnu" . "https://elpa.gnu.org/packages/")
   ("nongnu" . "https://elpa.nongnu.org/nongnu/")))

;; Download descriptions of available packages from the above
;; package archives.
(unless package-archive-contents
  (package-refresh-contents))

;; Install the keycast package if not already installed.
(dolist (package '(keycast))
  (unless (package-installed-p package)
    (package-install package)))

;; Enable keycast to show the current command and its binding in
;; the mode line, for the presentation.
(setopt keycast-mode-line-remove-tail-elements nil)
(when (fboundp #'keycast-mode-line-mode)
  (keycast-mode-line-mode 1))

;; Set a font with larger size for the presentation.
;; It requires that the Source Code Pro be installed on your
;; system.  Feel free to comment out or remove.
(when (display-graphic-p)
  (with-eval-after-load 'faces
    (let ((f "Source Code Pro Medium-15"))
      (set-face-attribute 'default nil :font f)
      (set-face-attribute 'fixed-pitch nil :font f))))

;; Inline function for expanding file and directory names inside
;; `user-emacs-directory'.  For example: (+emacs.d "gnus/")
(defsubst +emacs.d (path)
  "Expand PATH relative to `user-emacs-directory'."
  (expand-file-name
   (convert-standard-filename path) user-emacs-directory))

(keymap-global-set "C-c e e" #'eval-last-sexp)

;; Add the info directory from the GNU Emacs source repository to
;; the list of directories to search for Info documentation files.
;; Useful if you're using Emacs directly built from a source
;; repository, rather than installed on your system.
(with-eval-after-load 'info
  (setq
   Info-directory-list
   `(,@Info-directory-list
     ,(expand-file-name
       (convert-standard-filename "info/") source-directory)
     "/usr/share/info/")))


;;; Gnus configuration

;; (info "(gnus) Don't Panic")

(keymap-global-set "C-c g" #'gnus)

(setopt
 user-full-name    "Gnus Fan Emacsian"
 user-mail-address "ec25gnus@kelar.org")

;; Tell Emacs we'd like to use Gnus and its Message integration
;; for reading and writing mail.
(setopt
 mail-user-agent 'gnus-user-agent
 read-mail-command #'gnus)

;; Consolidate various Gnus files inside a gnus directory in the
;; `user-emacs-directory'.
(setopt
 gnus-home-directory (+emacs.d "gnus/")
 gnus-directory      (+emacs.d "gnus/news/")
 message-directory   (+emacs.d "gnus/mail/")
 nndraft-directory   (+emacs.d "gnus/drafts/"))

(setopt ; don't bother with .newsrc, use .newsrc.eld instead
 gnus-save-newsrc-file nil
 gnus-read-newsrc-file nil)

;; Don't prompt for confirmation when exiting Gnus.
(setopt gnus-interactive-exit nil)

;; Configure two IMAP mail accounts.
(setopt
 gnus-select-method '(nnnil "")
 gnus-secondary-select-methods
 '((nnimap
    "ec25gnus"
    (nnimap-stream tls)
    (nnimap-address  "mail.kelar.org")
    ;; (nnimap-server-port 993) ; imaps
    (nnimap-authenticator plain)
    (nnimap-user "ec25gnus@kelar.org"))
   (nnimap
    "ec25work"
    (nnimap-stream tls)
    (nnimap-address "mail.kelar.org")
    ;; (nnimap-server-port 993) ; imaps
    (nnimap-authenticator plain)
    (nnimap-user "ec25work@kelar.org")
    ;; Archive messages into yearly Archive folders upon pressing
    ;; 'E' (for Expire) in the summary buffer.
    (nnmail-expiry-wait immediate)
    (nnmail-expiry-target nnmail-fancy-expiry-target)
    (nnmail-fancy-expiry-targets
     (("from" ".*" "nnimap+ec25work:Archive.%Y"))))))

;; `init-file-debug' corresponds to launching emacs with --debug-init
(setq nnimap-record-commands init-file-debug)

;; The "Sent" folder
(setopt gnus-message-archive-group "nnimap+ec25gnus:INBOX")

;;;; Group buffer

;; Always show INBOX groups even if they have no unread or ticked
;; messages.
(setopt gnus-permanently-visible-groups ":INBOX$")
;; Enable topic mode in the group buffer, for classifying groups.
(add-hook 'gnus-group-mode-hook #'gnus-topic-mode)

;;;; Article buffer

;; Display the following message headers in Article buffers,
;; in the given order.
(setopt
 gnus-sorted-header-list
 '("^From:"
   "^X-RT-Originator"
   "^Newsgroups:"
   "^Subject:"
   "^Date:"
   "^Envelope-To:"
   "^Followup-To:"
   "^Reply-To:"
   "^Organization:"
   "^Summary:"
   "^Abstract:"
   "^Keywords:"
   "^To:"
   "^[BGF]?Cc:"
   "^Posted-To:"
   "^Mail-Copies-To:"
   "^Mail-Followup-To:"
   "^Apparently-To:"
   "^Resent-From:"
   "^User-Agent:"
   "^X-detected-operating-system:"
   "^X-Spam_action:"
   "^X-Spam_bar:"
   "^Message-ID:"
   ;; "^References:"
   "^List-Id:"
   "^Gnus-Warning:"))

;;;; Summary buffer

;; Fine-tune sorting of threads in the summary buffer.
;; See: (info "(gnus) Sorting the Summary Buffer")
(setopt
 gnus-thread-sort-functions
 '(gnus-thread-sort-by-number
   gnus-thread-sort-by-subject
   gnus-thread-sort-by-date))

;;;; Message and sending mail

(setopt
 ;; Automatically mark Gcc (sent) messages as read.
 gnus-gcc-mark-as-read t
 ;; Configure posting styles for per-account Gcc groups, and SMTP
 ;; server for sending mail.  See: (info "(gnus) Posting Styles")
 ;; Also see sample .authinfo file provided below.
 gnus-posting-styles
 '(("nnimap\\+ec25gnus:.*"
    (address "ec25gnus@kelar.org")
    ("X-Message-SMTP-Method" "smtp mail.kelar.org 587")
    (gcc "nnimap+ec25gnus:INBOX"))
   ("nnimap\\+ec25work:.*"
    (address "ec25work@kelar.org")
    ("X-Message-SMTP-Method" "smtp dasht.kelar.org 587")
    (gcc "nnimap+ec25work:INBOX"))))

(setopt
 ;; Ask for confirmation when sending a message.
 message-confirm-send t
 ;; Wrap messages at 70 characters when pressing M-q or when
 ;; auto-fill-mode is enabled.
 message-fill-column 70
 ;; Forward messages (C-c C-f) as a proper MIME part.
 message-forward-as-mime t
 ;; Send mail using Emacs's built-in smtpmail library.
 message-send-mail-function #'smtpmail-send-it
 ;; Omit our own email address(es) when composing replies.
 message-dont-reply-to-names "ec25\\(gnus\\|work\\)@kelar\\.org"
 gnus-ignored-from-addresses message-dont-reply-to-names)

;; Unbind C-c C-s for sending mail; too easy to accidentally hit
;; instead of C-c C-d (save draft for later)
(keymap-set message-mode-map "C-c C-s" nil)
;; Display a `fill-column' indicator in Message mode.
(add-hook 'message-mode-hook #'display-fill-column-indicator-mode)
;; Enable Flyspell for on-the-fly spell checking.
(add-hook 'message-mode-hook #'flyspell-mode)

Sample ~/.authinfo file:

machine ec25gnus login ec25gnus@kelar.org password hunter2
machine ec25work login ec25work@kelar.org password badpass123
machine mail.kelar.org login ec25gnus@kelar.org password hunter2
machine dasht.kelar.org login ec25work@kelar.org password badpass123

Note that for purpose of storing credentials for use by Gnus’s select methods, the machine portions need to match the names we give our select methods when configuring gnus-secondary-select-methods, namely ec25gnus and ec25work in our example.

We also store a copy of the credentials for use by Emacs’s smtpmail when sending mail, where the machine must be the fully-qualified domain name (FQDN) of the SMTP server we specify with the X-Message-SMTP-Method header for each account by defining a corresponding rule for it in gnus-posting-styles.

Lastly, I recommend using an encrypted authinfo file by saving it as ~/.authinfo.gpg instead to avoid storing your credentials in plain text. If you set up Emacs’s EasyPG, it will seamlessly decrypt or encrypt the file using GPG when reading from or writing to it. Type C-h v auth-sources RET to see the documentation of the auth-sources variable for more details.

-1:-- Reading and writing emails in GNU Emacs with Gnus (Post Amin Bandali)--L0--C0--2025-12-06T15:50:00.000Z

Jeremy Friesen: Managing Light/Dark Scheme in MacOS and Linux

On my personal machine I’m using Debian 📖 with the Gnome 📖 desktop. And my work machine runs MacOS. I have written an Emacs 📖 function (M-x jf/dark) that toggles between light and dark for either my personal machine or work machine. You can find the code up on Github.

First we have the general function (and associated alias for ease of typing):

(defun jf/color-scheme-system-toggle ()
  "Toggle system-wide Dark or Light setting."
  (interactive)
  (funcall
    (intern
      (format "jf/color-scheme-system-toggle:%s" system-type))))

(defalias 'jf/dark 'jf/color-scheme-system-toggle)

I’m opting to use a dispatch pattern, in which I dynamically construct the function name(s) to call based on the system-type variable. A disadvantage of this approach is that I’m defining functions for an Operating System (OS 📖) that is not relevant to the machine.

It would be simple to refactor, but for reasons of the example, I’ll keep them separate.

For themes I have the following:

(defvar jf/themes-plist
  '(:dark ef-owl :light ef-elea-light))

And I use the following command to set the theme based on the color scheme:

(defun jf/color-scheme:emacs (&optional given-scheme)
  "Function to load named theme."
  (let ((scheme
         (or given-scheme
             (funcall
              (intern
               (format "jf/color-scheme-func:%s" system-type))))))
    (modus-themes-select (plist-get jf/themes-plist scheme))))

For MacOS

I have the following:

(defun jf/color-scheme-system-toggle:darwin ()
  "Toggle the darwin system scheme."1’
  (shell-command
   (concat "osascript -e 'tell application \"System Events\" "
           "to tell appearance preferences "
           "to set dark mode to not dark mode'"))
  (jf/color-scheme-set-for-emacs))

To determine the MacOS color scheme:

(defun jf/color-scheme-func:darwin ()
  "Determine MacOS preferred/current theme."
  (if (equal "Dark"
             (substring
              (shell-command-to-string
               "defaults read -g AppleInterfaceStyle") 0 4))
      :dark :light))

For GNU/Linux with Gnome

The command for Linux and Gnome is as follows:

(defun jf/color-scheme-system-toggle:gnu/linux ()
  "Toggle the gnu/linux system scheme."
  (let* ((target_scheme
          (plist-get '(:dark :light :light :dark)
                     (jf/color-scheme-func:gnu/linux))))
    ;; Instead of all of the shelling out, we could assemble the shell
    ;; commands into a singular command and issue that.
    (dolist (setting jf/color-scheme-commands:gnu/linux)
      ;; In essence pipe the output to /dev/null
      (shell-command-to-string
       (format (plist-get setting :template)
               (plist-get setting target_scheme))))
    (jf/color-scheme:emacs target_scheme)))

The list of settings to change are as follows:

(defvar jf/color-scheme-system-toggle/gnome-settings
  '((:template "gsettings set org.gnome.settings-daemon.plugins.color night-light-enabled %s"
               :light "false" :dark "true")
    (:template "gsettings set org.gnome.desktop.interface color-scheme %s"
               :light "default" :dark "prefer-dark")
    (:template "gsettings set org.gnome.desktop.interface gtk-theme %s"
               :light "default" :dark "prefer-dark"))
  "A list of plists with three parts:

- :template :: command to run.
- :dark :: what the setting should be to be in \"dark\" mode.
- :light :: what the setting should be to be in \"light\" mode.")

To determine the current color scheme in Gnome:

(defun jf/color-scheme-func:gnu/linux ()
  "Determine Gnome preferred/current theme."
  (if (equal
        "'prefer-dark'"
        (s-trim
          (shell-command-to-string
            "gsettings get org.gnome.desktop.interface color-scheme")))
    :dark :light))
-1:-- Managing Light/Dark Scheme in MacOS and Linux (Post Jeremy Friesen)--L0--C0--2025-12-06T02:04:34.000Z

Kana: Juicemacs: Exploring Speculative JIT Compilation for ELisp

This Org-mode file was used for org-present presentation on EmacsConf 2025 – Juicemacs. The current blog post is modified from the presentation, with added transcript and explanations to a bunch of things I didn't dig into in the presentation.

emacsconf-logo1-256.png For EmacsConf 2025

Project: https://github.com/gudzpoz/Juicemacs

Contact: See the navigation bar (or join the Zulip chat)

Read more… (31 min remaining to read)

-1:-- Juicemacs: Exploring Speculative JIT Compilation for ELisp (Post Kana)--L0--C0--2025-12-05T01:00:00.000Z

Charles Choi: Import Markdown to Org with the Clipboard in Emacs

The preponderance of Markdown formatted text in both websites and apps is a blessing and a point of friction for Org users. The content is there, but if you’re an Org user, you really prefer the text to be formatted for Org. If you have Pandoc installed, converting from Markdown to Org is trivial but laborious as it involves making the right command invocation with temporary files.

Thankfully, Emacs can automate this conversion as described in the workflow below:

  • From the source app or website, copy the Markdown into the system clipboard. This step pushes this text into the Emacs kill-ring.
  • In Emacs, invoke a command which converts the above Markdown text into Org, then pastes (yanks) converted text into a desired Org file.

Note that this workflow presumes that the Emacs kill-ring is integrated with the system clipboard and that pandoc is installed.

The following Elisp command cc/yank-markdown-as-org implements the above workflow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(defun cc/yank-markdown-as-org ()
  "Yank Markdown text as Org.

This command will convert Markdown text in the top of the `kill-ring'
and convert it to Org using the pandoc utility."
  (interactive)
  (save-excursion
    (with-temp-buffer
      (yank)
      (shell-command-on-region
       (point-min) (point-max)
       "pandoc -f markdown -t org --wrap=preserve" t t)
      (kill-region (point-min) (point-max)))
    (yank)))

This command can be invoked in numerous ways including by key binding, context menu, Transient menu, and directly via M-x. My preference is using the context menu as illustrated in the screenshot below to optimize for mouse copy/paste interactions between different apps.

Users interested in seeing how I’ve configured my context menu can read the source here. Users unfamiliar with Emacs context menus can learn more about it in my post Customizing the Emacs Context Menu.

Closing Thoughts

Implementing cc/yank-markdown-as-org turned out to be a lot more simpler than expected, as the command shell-command-on-region does the heavy lifting in abstracting away the pandoc command invocation and its handling of temporary files. This implementation can serve as a example for other conversions using the kill-ring.

A word of thanks to mekeor on IRC for reminding me that (point-min) and (point-max) exist.

-1:-- Import Markdown to Org with the Clipboard in Emacs (Post Charles Choi)--L0--C0--2025-12-04T21:30:00.000Z

Irreal: Configuring AUCTeX

If you’re an Emacser who writes in LaTeX you’re doubtless familiar with AUCTeX. Most of the time, you can depend on Org mode to do the heaving lifting but when you have a complicated layout you may have to invoke AUCTeX.

Randy Ridenour, a professor at Oklahoma Baptist University, writes in LaTeX using AUCTeX and, of course, wanted to configure it using John Wiegley’s use-package macro. He did what we’d all do:

(use-package tex
    :ensure auctex)

but it didn’t work. It turns you have to do

(use-package tex-site
    :ensure auctex)

instead.

When I saw his post, it rang a bell so I checked my config and I do the exact same thing. I vaguely remember going through the same steps as Ridenour years ago. I no longer recall how I discovered the magic spell but I doubtless found it on the Web somewhere. Ridenour’s problem was getting things to work with Skim. I’ve never used Skim so I had some other problem but the answer was the same.

Once I fixed the problem, I just moved on and so have no idea why you need tex-site instead of tex. I’m sure one of Irreal’s well informed readers will jump in with the answer.

I know this trip down memory lane isn’t all that interesting to most of you but I’m writing about it for the same reason that Ridenour did: to help someone else struggling with the problem find the answer.

-1:-- Configuring AUCTeX (Post Irreal)--L0--C0--2025-12-04T16:19:11.000Z

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