Emacs@Habrahabr: [Перевод] Принципы работы интегрированных сред разработки

Вот некоторые принципы работы IDE (интегрированных сред разработки), которые, на мой взгляд, нужно зафиксировать:

Принцип минимальной навигации

Навигация в IDE раздражает. В тексте, конечно, можно выучить все причудливые навигационные сокращения типа C-a M-< M-f и так далее, но всё равно требуется немало набирать на клавиатуре. А если речь идет о навигации в файловой системе, то требуется еще больше ввода и поиска.

IDE должна минимизировать время, требуемое на навигацию. В частности, можно организовать автоматический импорт функции, когда пользователь набирает call-сайт, чтобы не переходить в верхнюю часть файла, чтобы добраться до импорта, или использование goto определения, чтобы не приходилось выяснять, в каком файле определен тот или иной класс.

Навигация также может быть связана с перемещением блоков кода, например, если вам нужно обратить оператор if, то это должно обеспечиваться на уровне кода, а не только при помощи манипуляций вручную.

Вы должны стремиться к тому, чтобы пользователи при наборе кода мыслили линейно. Вспомните, как современный траекторию набора текста, подобно тому, как современный ЦП-ориентированный код избегает ветвлений и переходов. Читать дальше →
-1:-- [Перевод] Принципы работы интегрированных сред разработки (Post ph_piter)--L0--C0--October 06, 2023 12:32 PM

Jeremy Friesen: Adjusting Emacs to Shift with a New Project Organization Strategy

Summary: A quick walk through of adjusting the projectile package to use fast file finding and not include git submodules in those file lists.

I rarely use Git 📖 submodules. My text editor workflow and mental heuristics assume that a “project” is one Git repository. I use projectile (with some Consult decoration) to find files within the project. It works great. Especially with the amazing consult-projectile package.

However in a new project Hyku 📖 we’ve started using Git submodules. I’ll have more on this novel approach at some future point. For now, it’s a prototype for extending an existing Ruby on Rails 📖 application.

With this change, when I look for a file via projectile I get a list that includes the files in the main repository and the files in the very large Git submodule. Confounding things is that the file names are often quite similar.

I spent an hour or so delving into how to remove submodule files from the file selection.

First, I discovered that by default, projectile’s indexing method is alien; meaning it uses external shell commands to build the list. Looking at the source code, I found that the alien code was using fd 📖 with the following args: -H -0 -E .git -tf --strip-cwd-prefix -c never. Stored in the projectile-git-fd-args variable.

To understand what those all meant, I looked to the man page for fd. The critical switch was the -E .git. That meant to exclude all files from the .git directory.

What I wanted was to also exclude the hyrax-webapp directory i.e. the Git submodule’s root directory.

I added the following to my project’s .dir-locals.el:

((nil . ((projectile-git-fd-args . "-H -0 -E hyrax-webapp -E .git -tf --strip-cwd-prefix -c never")))

And gave it a spin. I still saw the files. I did some further digging, and projectile also has discovery of files in Git submodules. So I needed to configure that.

By default the shell command, stored in projectile-git-submodule-command is git submodule --quiet foreach 'echo $displaypath' | tr '\n' '\0'.

So to work around that, I amended my .dir-locals.el:

((nil . ((projectile-git-fd-args . "-H -0 -E hyrax-webapp -E .git -tf --strip-cwd-prefix -c never")
         (projectile-git-submodule-command . "")))

And with that (and updating the allowed dir-locals variables), I no longer see that submodules files in the parent project’s file list.

-1:-- Adjusting Emacs to Shift with a New Project Organization Strategy (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--October 05, 2023 12:28 AM

Jeremy Friesen: Emacs Function to Calculate an Errant's Movement Dice

Summary: In this post, I share Errant’s Chase procedure. I then write out the mathematics of calculating the movement dice. And from there provide an Emacs function for calculating movement dice. The chase subsystem is too interesting to ignore the movement dice. The underlying question is “should we care about the movement dice in combat?” I think so, because those dice add to the unpredictability of the game.


, I released Errant Encumberance, Speed, and Movement Dice Calculator. I also updated my script to reflect the same logic.

I was reading Errant 📖 and got curious about the mathematic behind the fiddly Movement Dice. A character’s Movement Dice are derived from their physique, skill, and number of item Slots carried.

The Movement Dice is a proud and odd little rule that impacts movement in combat as well as chases. My conjecture is that the chase mechanic encourages you to throw down your backpack and run; multiple dice are advantageous for escaping.

Of important note, the only time a character’s movement dice matter is during a chase and during combat. Exploration is not based on the movement dice.

Errant’s Chase Procedure

First let’s look at the Chases section of Errant :

In the case where they are being chased through a dungeon or similarly defined area, or for a short pursuit, the hunt can play out using standard initiative turn rules. However, for longer pursuits, and ones that may take place in broadly abstracted spaces like the wilderness or in cities, the following chase procedure can be used.

In a chase, generally, the participants can be tracked in terms of what side they’re on (i.e. pursuers and fugitives), but some chases may involve multiple parties or characters that need to be tracked separately.

Each initiative turn, the character with the lowest spd or mv on each side makes a movement roll. If the characters are on mounts or vehicles, use the spd of the mount or vehicle.

If the fugitives roll two 4’s, then they escape and the chase ends. If the pursuers roll two 4’s, they have caught the fugitives. In case of a tie, both sides make a movement roll as a tiebreaker.

If either side rolls doubles that are not 4’s, then characters on that side may make melee attack roll, perform a sorcery or miracle, or any other actions they wish.

If any of the results on both side’s movement dice match each other, characters may make ranged attack rolls against the other side. So if the fugitives rolled a 3 and a 4, and the pursuers rolled a 2 and a 3, the 3’s match, and so characters on each side may make ranged attack rolls against characters on the other side.

Dropping items during a chase is a free action. Dropping something the pursuers are interested in (food, money, etc.) may force a morale roll to see if they continue the chase.

Characters on either side may choose to sprint, rolling double their normal amount of movement dice, but they must make a phys check with a dv equal to their encumbrance to do so. If they fail, they may not make a movement roll this initiative turn.

Characters on either side can choose to split off from their group; they will make movement rolls separately.

At the end of the initiative turn, if the chase has not yet ended, the side that rolled the lowest on their movement roll rolls a d10 for a chase development that affects them.

Ava Islam, Errant

We have some high stakes dice rolls where having more movement dice increases your chances of ending the chase or taking meaningful actions during the chase. There’s a press your luck element in choosing to sprint (and garner potentially more dice). There’s also the option to split from the slower characters.

All modeling the absolute chaos of a good old fashioned rout.

On to the Mathematic of It All

With that in mind, we dive into the fiddly bits. We use two attributes (e.g. physique and skill) plus the more fluid number of carried Slots.

Errant encourages you to calculate your Spd while carrying your backpack and when not carrying your backpack. The book wants to make it clear just how much that backpack is costing you during a chase.

A character’s ENC (or Encumbrance) is:

  • When slots > physique, then 4 + slots - physique
  • Else (4 × slots) ÷ physique rounded down.

A character’s SPD (or Speed) is skill - encumberance. And their movement die is speed + 4 rounded down.

Emacs Lisp Function

The following function prompts for information and provides movement dice while carrying your backpack and when not carrying your backpack. I suppose it would also be useful to see the effect of discarding things carried in your hands. Because when you’re running maybe discarding that greatsword is a better plan than carrying the dead weight.

(defun jf/gaming/errant/movement-dice (prefix)
  "Calculate an Errant's movement dice."
  (interactive "P")
  (if prefix
      (shell-command (concat "open ~/git/takeonrules.source/static/errant/index.html"))
    (let* ((range '("4" "5" "6" "7" "8" "9" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "20"))
           (slot-range '("0" "0.25" "0.5" "0.75" "1" "1.25" "1.5" "1.75" "2" "2.25" "2.5" "2.75"
                         "3" "3.25" "3.5" "3.75" "4" "4.25" "4.5" "4.75" "5" "5.25" "5.5" "5.75"
                         "6" "6.25" "6.5" "6.75" "7" "7.25" "7.5" "7.75" "8" "8.25" "8.5" "8.75"
                         "9" "9.25" "9.5" "9.75" "10" "10.25" "10.5" "10.75" "11" "11.25" "11.5" "11.75"
                         "12" "12.25" "12.5" "12.75" "13" "13.25" "13.5" "13.75" "14"
                         "14.25" "14.5" "14.75" "15" "15.25" "15.5" "15.75" "16" "16.25" "16.5" "16.75" "17"
                         "17.25" "17.5" "17.75" "18" "18.25" "18.5" "18.75" "19" "19.25" "19.5" "19.75"
                         "20" "20.25" "20.5" "20.75" "21" "21.25" "21.5" "21.75" "22"
                         "22.25" "22.5" "22.75" "23" "23.25" "23.5" "23.75" "24"))
	   (phys (string-to-number (completing-read "Physique: " range nil t)))
	   (skil (string-to-number (completing-read "Skill: " range nil t)))
	   (slots-hand (string-to-number (completing-read "Slots in hand: " (subseq slot-range 0 9) nil t)))
	   (slots-handy (string-to-number (completing-read "Slots in handy: " (subseq slot-range 0 17) nil t)))
	   (slots-worn (string-to-number (completing-read "Slots in worn: " slot-range nil t)))
	   (slots-pack (string-to-number (completing-read "Slots in pack: " slot-range nil t)))
           (text (format "Errant Movement\n- Physique: %s · Skill: %s\n- Slots Hand: %s · Handy: %s · Worn: %s · Pack: %s"
			 phys skil slots-hand slots-handy slots-worn slots-pack)))
      (dolist (label-slots (list (cons "in hand, handy, worn, pack"
				       (+ slots-hand slots-handy slots-worn slots-pack))
				 (cons "in hand, handy, worn"
				       (+ slots-hand slots-handy slots-worn))
				 (cons "handy, worn"
				       (+ slots-handy slots-worn))
				 (cons "worn" slots-worn)
				 (cons "naked and free" 0)))
        (let* ((slots (cdr label-slots))
               (label (car label-slots))
               (enc (if (>= phys slots)
		        (floor (* 4 slots) phys)
		      (+ 4 (- (floor slots) phys))))
               (spd (if (>= skil enc) (- skil enc) 0))
               (md (floor spd 4))
               (md-text (if (= 0 md) "0" (format "%sd4" md))))
          (setq-local text (format "%s\n- %s\n  ENC: %s · SPD: %s· MD: %s"
				   text label enc spd md-text))))
      (kill-new text)
      (message text))))


I like the chase procedure; and the fiddly bits make the procedure possible. Also given that combat might lead to chase, having a clear understanding of a character’s movement dice helps folks think about the viability of fleeing.

If I don’t want to worry about it, a quick and dirty method could be:

  • Pick the higher of physique or skill and subtract the slots.
  • Select the lowest sided-die from the dice chain that has sides greater than the above amount.
  • Roll that die and multiply by 10 feet.

Or just move 10 feet per point of difference.

-1:-- Emacs Function to Calculate an Errant's Movement Dice (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--September 27, 2023 09:43 PM

Jeremy Friesen: A History of the Text Editors I've Used

Summary: I take a tour of the text editors I’ve used and why and when I moved on from them. Along the way, I write up some further insights into what I’m looking for and how I practice and explore a new to me editor.

I started working with open-source software in . Having come from a Microsoft Windows and IBM System iSeries 📖 environment, I was accustomed to my tools being vendor provided.

In I began my quest to find the software for editing my code. I looked into JEdit, TextWrangler, and BBEDit. I even tried Emacs 📖 . I’ve often wondered what would be different had I chosen Emacs in . But back then, I didn’t have patience for a tutorial While I was looking and using these different editors, I learned about Ruby on Rails 📖 and saw folks using Textmate 📖 .

I adopted Textmate ; it felt intuitive and in those earlier days of Ruby on Rails it was the defacto editor of so many Rubyists.

To this day, the function of line filter is one that I continue to consider necessary for my editing experience. In Emacs , I use consult-lines to replicate this behavior.

In Textmate, I wrote a few snippets, but never a plugin. I did spend time crafting a refined Railscast theme.

Sometime in or I switched from Textmate to Sublime Text 📖 . The primary reason for switching was Textmate’s slowness in searching within a large project. I brought forward my Railscast theme.

In , when I joined Hesburgh Libraries 📖 and Samvera 📖 , I had access to Rubymine. I gave that a whirl, but the heavy foot print of an Integrated Development Environment (IDE 📖) worked contrary to my approach.

Also, given that I was writing blog posts,

In , I switched from Sublime Text to Atom 📖 . The foundational reason was that Atom was open-source. I again brought forward my modified Railscast theme. https://github.com/jeremyf/vibrant-ink-ruby I remember practicing Vim 📖 but the paradigm did not stick. I continue to practice on occasion, mostly because remote machines invariably have Vim installed.

I began exploring writing plugins and themes in Atom. I found it clunky as you needed to write configuration/functions in multiple files.

During , I began noticing that Atom was more prone to breakage. And I didn’t like the slow boot time for Atom; especially when I had my GIT_EDITOR configured to atom -w; I would type git commit on the command-line and wait for Atom to boot up so I could type a commit message.

In , I grew tired of Atom’s flakiness, and saw that with Microsoft’s purchase of Github that Visual Studio Code (VS Code 📖) would likely be getting attention and Atom would further languish. It was that Github announced they were Sunsetting Atom.

I began my quest for a new editor. I wrote Principles of My Text Editor and Revisiting the Principles of My Text Editor; both being snapshots of my understanding. It wasn’t until watching Things Your Editor Should Have with Amir Rajan and Responding to “Things Your Editor Should Have” that I truly refined my understanding of my relationship to my text editor.

On I stopped using Atom and started using and once again practicing Vim . Humans, I swear I have tried many times to learn and use Vim. I can fumble around, but modal editing does not work for me.

On , I stopped trying to make Vim my editor and started exploring VS Code . It reminded me of Atom, Textmate, and Sublime. But there were two things I couldn’t get over:

  1. The default window dressing felt like I might accidentally charge my credit card at some point.
  2. The default commit message input is an HTML type=text element. A design consideration that encourages writing very little for commit messages.

I’ve since learned that configuring the window dressing is somewhat trivial. But that pernicious commit message input remains.

And why was that a big deal? My thinking was that any code editor that has a small input area for entering commit messages was built on the principles that history does not matter.

And would propagate subtle bad habits; such as modeling that folks need not write good commit messages. Good commit messages help expose intent and constraints.

I set aside VS Code and looked for other options. Emacs was barely a consideration. But I was running out of viable options.

I spent time practicing, namely working through the tutorial and exploring package archives to find utilities to help me write.

On I switched to Emacs. I tried both Doom and Spacemacs 📖 , but settled on building my own configuration.

What this looked like was starting from a baseline install and just writing and coding. When I hit a point where I knew past editors did something, I would begin exploring. I did this with the mindset of “I am learning and building my tools; so other things will move slower.” In other words, if I had an urgent task to complete, I wouldn’t do this. But if I had some slack in time, I’d take time. Also it was ; I had lots of slack time.

The most fruitful exploration came when I wanted to use the line filter function that was part of Textmate, Sublime, and Atom.

This lead me to Ivy, Counsel, and Swiper; this trio helped me understand Emacs. I could see the interactive functions and their first line doc-strings. That discoverability, paired with Helpful helped me build steam.

Eventually, I switched from the trio of Ivy, Counsel, and Swiper to the quintet of Consult, Marginalia, Vertico, Orderless, and Embark. The reason? The package sizes and design considerations. Namely the quintet of packages built atop the Emacs Application Programming Interface (API 📖) ; creating what is likely to be less maintenance overhead.

As I watch folks use their editors, I’ve discovered one small bit of functionality that Emacs has versus other editors. The concpet of a buffer, separate from the file. I can create a Scratch Buffer to write down a thought or bit of code. I don’t need to fiddle with creating a new file and deciding where that file goes. The Scratch Buffer serves as a somewhat durable place that favors capturing the idea over determining where to file that idea.

I know for myself, if I’m working on something then have an idea partially related, I would lose a lot of concentration and focus if I also had to specify where I write that idea.


These days, I struggle to think of what would prompt me to move from Emacs. I’ve created an environment that works for me; and when I begin to see repeated tasks I spend a bit of time exploring ways to encode them.

Creating functions and packages in Emacs feels far less arduous than it did in Atom and VS Code . I’m extending my editor at a slower frequency, but that’s a function of maturity.

-1:-- A History of the Text Editors I've Used (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--September 03, 2023 04:28 PM

Jeremy Friesen: Musing on Emacs

Summary: The tools I use for my work are a vital part of me. I spend evenings talking about them with a dear friend. I encourage other folks to practice their tools, hoping that in practicing folks will improve their problem solving skills.

On Thursdays I often have dinner and drinks with my good friend Brandon. We talk about books, music, Zelda, travel, general programming, and Emacs 📖 . He’s been using Emacs for decades, whereas I’m a recent convert. Since .

This we were again talking about Emacs . And Brandon wondered the following: “It would be interesting to have a set of tasks we needed to accomplish in the same document, and see how differently we approach the solution.”

We haven’t ever done any pair programming together, but know that our approaches to navigating and editing are different. Because Emacs is an editor that flows and moves with you. And Brandon and I adopted Emacs more than two decards apart; so his habits and approaches will likely vary from mine. In part because of the available packages we’ve each adopted and practiced.

More than any other editor I’ve used, I spend time honing and practicing Emacs . Part of this is the joy of discovery and thinking through different approaches.

Over on Reddit, someone asked “Convince me to stay with Emacs?!My response was as follows:

I’m a recent convert to Emacs (started in 2020). I’ve been a professional software developer for years. I gave VS Code a whirl back in 2020, but it didn’t suit me.

What I like about Emacs is that I have an integrated computing environment: writing, reading, and coding. When I get better at using my tools, all aspects benefit. At https://takeonrules.com/about/stats/ you can see an overview of my pre-Emacs stats versus my with Emacs. It’s a game changer for me.

The other bit of Emacs that I adore is how quick it is to extend my editor. I can record a macro or write a function, and save that little snippet of code for future usage. It’s easy to temporarily assign a key combination to a function.

I wrote a bit about that here: https://takeonrules.com/2023/08/25/using-emacs-s-tramp-to-edit-files-on-a-rancher-backed-kubernetes/

I encourage you to watch “Things Your Editor Should Have with Amir Rajan”: https://www.youtube.com/watch?v=lPjPa_yqM9g and give a read of Prot’s response https://protesilaos.com/codelog/2023-04-09-comment-emacs-rubber-duck-show/

I wasn’t aiming to convince the original poster but instead to get them thinking about their editor.

A question I like to ask the people I mentor, “How many more years do you think you’ll be writing code? Writing in general? Do you think at least for the next 5 years?” All of that is a nudge for them to practice using their tools. A reminder that the tools we use shape what we view as possible or feasible.

-1:-- Musing on Emacs (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--September 02, 2023 08:02 PM

Jeremy Friesen: Using Emacs’s Tramp to Edit Files on a Rancher-backed Kubernetes

Summary: In this post I walk through a problem I had regarding editing remote files. I describe an interim solution and then how I used kubectl and tramp to later edit the files “locally.”

This week I’ve been wrestling with an integration environment challenge. I need to debug code on a remote demo instance that is configured similar to production. Due to the current constraints, this involves using Secure Shell (SSH 📖) to edit remote files in a Kubernetes (K8s 📖) cluster, then restarting the web-server for the cluster, and finally checking logs to see what’s happening.

To say it is tedious is an understatement.

Initial Solution

Further confounding the problem was that I needed to make these changes using Vim 📖 . I like Vim but my Emacs 📖 key bindings continually get in the way.

My initial work around was to create a simple interactive Emacs function:

(defun jf/rancher/rm-then-vim-project-file (&optional filename)
  "Kill some text to edit a FILENAME in Rancher."
  (let* ((f (or filename (buffer-file-name)))
          (concat "./"
                  (file-relative-name f (projectile-project-root)))))
    (kill-new (f-read f))
    (kill-new (format "rm %s ; vim %s"
                      relative-name relative-name))))

The above copies the current file contents. Then copies the command to remove the old file and edit the file in Vim . The idea being that while I’m in the web User Interface (UI 📖) , I can past the rm ; vim command to open a clean file and then paste the contents into it. It’s a bit of a lazy-person’s scp .

Let’s Go Tramp

I have wanted to be able to SSH into a K8s pod. I spent a bit of time and these are my steps for leveraging Emacs ’s Tramp package. We use Rancher 📖 for our K8s management.


This builds on How to SSH into a Rancher Node and Getting Started With Rancher 2

  • brew install kubectl (for controlling K8s )
  • brew install krew (for adding plugins)
  • brew install fzf (for some fuzzy finding friendlyness)
  • kubectl krew install ctx
  • kubectl krew install konfig
  • kubectl krew install ns
  • Ensure that ~/.krew/bin is in your $PATH
  • Ensure that ~/.kube/config file exists (e.g. touch ~/.kube/config)


Copy the contents of Rancher’s cluster’s KubeConfig into a local file. As of the config is available in the top-right menu of the cluster. For this example, we’ll write the file to ~/kubeconfig.tmp.yml

With that file, add the config to your kubectl (e.g. kubectl konfig import --save ~/kubeconfig.tmp.yml).

Run kubectl config get-contexts; you should see the cluster you just added to the kubectl konfig.

Run kubectl ctx and set the current context to the cluster you added.

Run kubectl get pods; you should see the named pods. In the case of our Hyku 📖 builds you’ll see something along the lines of the following:

❯ kubectl get pods
NAME                                           READY   STATUS    RESTARTS   AGE
palni-palci-demo-nginx-66d86c56c5-pdzn4        1/1     Running   0          11h
palni-palci-demo-pals-84bc48c974-64ztz         1/1     Running   0          69m
palni-palci-demo-pals-84bc48c974-pnvnn         1/1     Running   0          67m
palni-palci-demo-pals-worker-c54c8d69d-pd9p4   1/1     Running   0          69m
palni-palci-demo-redis-master-0                1/1     Running   0          6d7h

Tramp-ing to Those Files

With all of that pre-amble, in Emacs M-x find-file and type /kubernetes:palni-palci-demo-pals-84bc48c974-pnvnn:/. The trailing :/ indicates that we’re looking in the default directory, and with a Rails application you’ll see the usual suspects for directories (e.g. app/ lib/ etc.); begin navigating and editing those files.

If I find myself doing this quite regularly, I’ll likely want to create some helper functions for the find-file.

-1:-- Using Emacs’s Tramp to Edit Files on a Rancher-backed Kubernetes (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 25, 2023 07:21 PM

Jeremy Friesen: Extracted the Random Table to an Independent Package

Summary: Introducing random-package.el, recently extracted from my personal Emacs configuration.

, after work, I extracted my random-table.el package to its own repository.

Over the last few days, I’d been writing about this package. Below are the blog posts I wrote before the extraction:

I’ll file further blog posts in my Emacs random-table.el Package series.

-1:-- Extracted the Random Table to an Independent Package (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 21, 2023 11:47 PM

Jeremy Friesen: Adding Rudimentary Handling of Math Operands in Random Table Package

Summary: Explaining new functionality added to my random-table.el package. This explanation includes the Emacs Lisp code and some explanation. Ultimately, building these random tables grows my personal GM notebook; encoding logic and making it readily and consistently available (when I have my computer).

I wrote about Adding Optional Dice Roll Prompting to my Emacs Random Table.


, I extended the existing functionality with two features:

  • Exclude a table from prompting for a roll.
  • Allow for rudimentary math operands with table results.

Exclude a Table from Prompting for a Roll

Prior to my most recent change, when you pass the universal prefix arg to random-table/roll it would always prompt for rolls on the table. Even if there was one record in the :data element. You would get a prompt that looked like: “Roll 1d1 on Your Named Table:”

With the most recent change, any table that has one element in :data will not prompt for the roll. Also, you can specify :exclude-from-prompt t when registering a table; then any “rolls” on that specific table will not prompt to give the dice value.

Ultimately, the goal is to ask for dice rolls when they might be something the player wants to roll.

Allow for Rudimentary Math Operands with Table Results

In my quest for more random tables and functionality, I worked through Errant 📖 ’s Hiring Retainers section. Using the Player Character (PC 📖) ’s presence, you look-up the morale base. Then roll 2d6, modified by the offer’s generosity, to then determine the modifier to the morale base.

To perform mathematical operations, I continue to leverage the s-format functionality. That is s-format will evaluate and replace the text of the following format: ${text}.

Below is the definition of a random Henchman for Errant. I do need to consider randomizing it’s level; higher level henchmen are available at larger urban centers.

 :name "Henchman (Errant)"
 :data '("\n- Archetype :: ${Henchman > Archetype (Errant)}\n- Morale :: ${[Henchman > Morale Base] + [Henchman > Morale Variable]}"))

The ${Henchman > Archetype (Errant)} will look on the following table:

 :name "Henchman > Archetype (Errant)"
 :private t
 :roller #'random-table/roller/1d10
 :data '(((1 . 5) . "Warrior")
         ((6 . 8) . "Professional")
         ((9 . 10) . "Magic User")))

The ${[Henchman > Morale Base] + [Henchman > Morale Variable]} does the following:

  • Roll on Henchman > Morale Base
  • Roll on Henchman > Morale Variable
  • Add those two results together.
 :name "Henchman > Morale Base"
 :private t
 :roller (lambda (table) (read-number "Hiring PC's Presence Score: "))
 :data '(((3 . 4) . 5)
         ((5 . 8) . 6)
         ((9 . 13) . 7)
         ((14 . 16) . 8)
         ((17 . 18) . 9)
         ((19 . 20) . 10)))

 :name "Henchman > Morale Variable"
 :private t
 :roller (lambda (table)
           (let* ((options '(("Nothing" . 0) ("+25%" . 1) ("+50%" . 2) ("+75% or more" . 3)))
                  (key (completing-read "Additional Generosity of Offer: " options))
                  (modifier (alist-get key options nil nil #'string=)))
             (+ modifier (random-table/roller/2d6 table))))
 :data '(((2) . -2)
         ((3 . 5) . -1)
         ((6 . 8) . 0)
         ((9 . 11) . 1)
         ((12 . 15) . 2)))

Diving into the Parser

Adding this functionality was somewhat quick. I stumbled on the match-string-no-properties; when I did not pass the text it used the positional data of string-match to look at the buffer’s text. In other words, it was broken without this. Below is the code that I added for replacer function logic. The \\[\\(.*\\)\\][[:space:]]*\\(-\\|\\+\\|\\*\\)[[:space:]]*\\[\\(.*\\)\\] Regular Expression 📖 is a lot to visually parse.

There are three capture regions:

  • First: The text within square brackets that is to the left of the operand (e.g. second capture region)
  • Second: The mathematical operand (e.g. -, +, or *; no division just yet)
  • Third: The text within square brackets that is to the right of the operand (e.g. second capture region)

We use Emacs 📖 ’s apply function to call the operand as a function. The left operator being the rolled results of the first capture region. The right operator being the results of rolling the third capture region.

((string-match "\\[\\(.*\\)\\][[:space:]]*\\(-\\|\\+\\|\\*\\)[[:space:]]*\\[\\(.*\\)\\]" text)
        (let* ((table-one (match-string-no-properties 1 text))
               (operator (match-string-no-properties 2 text))
                (table-two (match-string-no-properties 3 text)))
          (apply (intern operator)
              (string-to-number (random-table/roll/parse-text/replacer table-one))
              (string-to-number (random-table/roll/parse-text/replacer table-two))))))

Having mathematical operators assumes that the tables are returning number like values.

When registering tables, I now disallow [ and/or ] in the :name of the table. This way I can be more confident that the ${[Table] + [Table Two]} will result in operand logic.


Part of my ongoing reason for doing this is to build up my “Game Master (GM 📖) notebook” of tables and procedures. In my experience, any of these tables might be useful for inspiration or reducing rules lookup during play.

-1:-- Adding Rudimentary Handling of Math Operands in Random Table Package (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 20, 2023 06:08 PM

Emacs@Habrahabr: [Перевод] Знакомство с упорядочиванием в Emacs на примерах

В этой статье мы разберём использование в Emacs различных команд для упорядочивания строк, абзацев и страниц. В руководствах к Emacs и Elisp эти команды описаны достаточно подробно, так что мы просто познакомимся с практическими примерами их применения. Читать дальше →
-1:-- [Перевод] Знакомство с упорядочиванием в Emacs на примерах (Post Bright_Translate)--L0--C0--August 20, 2023 10:00 AM

Jeremy Friesen: Adding Optional Dice Roll Prompting to my Emacs Random Table

Summary: Adding dice rollers that allow for either programatic randomization or prompting the user to manually roll dice and enter the results. A side benefit, is that this feature can help with debugging.

I wrote Further Into My Random Table Emacs Proto Package. In writing it I thought up the following:
First, invoke the roller with one universal prefix arg (e.g. C-u M-x random-table/roll) to replace all random calls with prompts. That is “Computer asks for a die roll, human provides it, and then evaluates further.”

The default behavior I want is that when I roll on a table Emacs 📖 will quietly “roll the underlying dice.” However, if I pass the universal prefix arg (e.g C-u M-x random-table/roll) then Emacs will prompt me to enter each of the dice rolls.


Below is the code changes to either silently roll or prompt:

;;; Random Table Roller
(cl-defmacro random-table/roller (&rest body &key label &allow-other-keys)
  (let ((roller (intern (concat "random-table/roller/" label)))
         (docstring (format "Roll %s on given TABLE" label)))
    `(defun ,roller (table)
       (if current-prefix-arg
         (read-number (format "Roll %s for %s: " ,label (random-table-name table)))

(defun random-table/roller/default (table)
  "Given the TABLE roll randomly on it.

See `random-table/filter/default'.
See `random-table/roller' macro."
  ;; Constant off by one errors are likely
  (let ((faces (length (-list (random-table-data table)))))
    (if current-prefix-arg
      (read-number (format "Roll 1d%s for %s: " faces (random-table-name table)))
      (+ 1 (random faces)))))

;;;; Perhaps not ideal to have one function per roll type.  But...having
(random-table/roller :label "1d6" (+ 1 (random 6)))
(random-table/roller :label "2d6" (+ 2 (random 6) (random 6)))
(random-table/roller :label "1d12" (+ 1 (random 12)))
(random-table/roller :label "1d20" (+ 1 (random 20)))

Let’s create a quick table:

   :name "Random Attribute"
   :data '("Strength"

Given that I pass the universal prefix arg (e.g. C-u) when I roll on the Random Attribute table then I will get the prompt “Roll 1d6 for:” and the value I enter will be used for looking up the correct :data element.

Why Introduce This?

First, this refactor was a 30 minute exercise. I spent the most time tracking down the ,@body syntax. In the case of the random-table/roller macro, the body is all parameters that are not named.

Let’s say we have (random-table/roller :label "1d12" (+ 1 (random 12))). The body is (+ 1 (random 12)). And the @body syntax is the non-evaluated textual value of body. In other words, @body is not an integer but is instead the expression.

Second, when I’m playing a solo game, I’m often content to let Emacs silently roll the dice. However, in encoding the Death and Dismemberment table I can envision a case where I’m running a game and someone else’s character drops to 0 Hit Point (HP 📖) . I’d imagine that they’d want to roll their fate.

Third, this small scale refactoring helps reinforce the flexibility of the package; that I’ve adequately modeled the process flow and can amend each of the functional points.

Fourth, by adding the ability to specify the random value, it can ease the debugging challenges. In my experience properly testing functions that use randomization to generate output will often require that you not randomize but instead say what the random numbers will be.


I have a few more ideas and see a few edge cases; namely the current_roll functionality. I introduced that, but it’s inadequate. And I need to explore more around it. Namely I want the current_roll for the table on which this was written. Resolving that is necessary for having over-lapping ranges on tables.

I also want to go hunting for some more random tables. One that I’m considering is going so far as to make a random Old School Essentials (OSE 📖) character using the quick equipment guide from Carcass Crawler Volume 1, Issue 2. I’ve started sketching out how I might capture attribute values, then find qualifying classes and picking one. However, this thought exercise is a large chunk of work for the random-table package.

-1:-- Adding Optional Dice Roll Prompting to my Emacs Random Table (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 20, 2023 04:02 AM

Jeremy Friesen: Further Into My Random Table Emacs Proto Package

Summary: This post walks through encoding a new random table using my Emacs proto-package “random-table.el”. To get the tables working, I needed to do a bit of light refactoring. Ultimately, this example further demonstrates the flexibility of table composition.

Any sufficiently advanced hobby is indistinguishable from work.
Jeremy Friesen


On I wrote Emacs Proto Package for Random Tables. Since then, I’ve been looking at how to further extend this functionality.

For this blog post, I’m assuming you are referencing this version of the random-table package.

During episode 6 of 3d6 Down the Line 📖 ’s actual play of The Halls of Arden Vul 📖 , they used the Death and Dismemberment table from Goblin Punch.

I checked out the table. What I liked about the Death and Dismemberment table is that it eliminates death at 0 Hit Point (HP 📖) and introduces a sub-system that allows for recovery and addresses the damage in excess of current remaining HPs .

Encoding a New Table

I set about encoding the Death and Dismemberment rules for my Random Table package.

This required a few changes:

  1. I needed the concept of a current_roll. The Death and Dismemberment table.
  2. I wanted dice to be able to return strings and then use those strings as the lookup on the table’s :data.

I did not, at present, worry about the cumulative effects of data. However, I’m seeing how I might do that.

Let’s dig in.

There are five tables to consider for Death and Dismemberment:

  • Physical
  • Acid/Fire
  • Eldritch
  • Lightning
  • Non-Lethal

Here’s how I set about encoding that was as follows:

(random-table/register :name "Death and Dismemberment"
  :roller #'random-table/roller/prompt-from-table-data
  :data '(("Physical" . "${Death and Dismemberment > Physical}")
           ("Acid/Fire" . "${Death and Dismemberment > Acid/Fire}")
           ("Eldritch" . "${Death and Dismemberment > Eldritch}")
           ("Lightning" . "${Death and Dismemberment > Lightning}")
           ("Non-Lethal" . "${Death and Dismemberment > Non-Lethal}")))

The :roller is a function as follows:

(defun random-table/roller/prompt-from-table-data (table)
   (format "%s via:" (random-table-name table))
   (random-table-data table) nil t))

In the case of passing the Death and Dismemberment table, you get the following prompt: “Death and Dismemberment via”. And the list of options are: Physical, Acid/Fire, Eldritch, Lightning, and Non-Lethal.

Once I pick the option, I then evaluate the defined sub-table. Let’s look at Death and Dismemberment > Physical.

(random-table/register :name "Death and Dismemberment > Physical"
  :roller (lambda (table) (+ 1 (random 6)))
  :private t
  :data '(((1) . "Death and Dismemberment > Physical > Arm")
           ((2) . "Death and Dismemberment > Physical > Leg")
           ((3 . 4) . "Death and Dismemberment > Physical > Torso")
           ((5 . 6) . "Death and Dismemberment > Physical > Head")))

This is a rather straight-forward table. Let’s say the :roller returns a 5. We will then evaluate the Death and Dismemberment > Physical > Head table; let’s look at that. The resulting table is rather lengthy.

(random-table/register :name "Death and Dismemberment > Physical > Head"
  :roller #'random-table/roller/death-and-dismemberment/damage
  :private t
  :data '(((1 . 10) . "Head Injury; Rolled ${current_roll}\n- +1 Injury\n- Concussed for +${current_roll} day(s).")
           ((11 . 15) . "Head Injury; Rolled ${current_roll}\n- +1 Injury\n- Concussed for +${current_roll} day(s).\n- One Fatal Wound.\n- ${Save vs. Skullcracked}")
           ((16 . 1000) . "Head Injury; Rolled ${current_roll}\n- +1 Injury\n- Concussed for +${current_roll} day(s).\n- ${current_roll} - 14 Fatal Wounds.\n- ${Save vs. Skullcracked}")))

The :roller (e.g. random-table/roller/death-and-dismemberment/damage) is as follows:

(defun random-table/roller/death-and-dismemberment/damage (&rest table)
  (+ 1
     (random 12)
     (read-number "Number of Existing Injuries: " 0)
     (read-number "Lethal Damage: " 0)))

We roll a d12, add the number of existing injuries, and accumulated lethal damage. Then look up the result in the :data of Death and Dismemberment > Physical > Head. Let’s say the result is a 12. Put a pin in this result, we’re going to dive into some additional tables. We’ll need to roll on the the Save vs. Skullcracked table, which I’ve included below:

(random-table/register :name "Save vs. Skullcracked"
  :roller #'random-table/roller/saving-throw
  :private t
  :data '(("Save" . "Saved against cracked skull…gain a new scar.")
           ("Fail" . "Failed to save against cracked skull.  ${Save vs. Skullcracked > Failure}")))

The :roller (e.g. random-table/roller/saving-throw) will prompt for the saving throw score and any modifier to the roll. Then it will return “Fail” or “Save” depending on the results. See the function.

(defun random-table/roller/saving-throw (table)
  (let ((score (read-number (format "%s\n> Enter Saving Throw Score: " (random-table-name table)) 15))
         (modifier (read-number (format "%s\n> Modifier: " (random-table-name table)) 0))
         (roll (+ 1 (random 20))))
      ((= roll 1) "Fail")
      ((= roll 20) "Save")
      ((>= (+ roll modifier) score) "Save")
      (t "Fail"))))

Let’s say that we “Fail” the saving throw. We now lookup on the Save vs. Skullcracked > Failure table:

(random-table/register :name "Save vs. Skullcracked > Failure"
                       :private t
                       :data '("Permanently lose 1 Intelligence."
                               "Permanently lose 1 Wisdom."
                               "Permanently lose 1 Charisma."
                               "Lose your left eye. -1 to Ranged Attack."
                               "Lose your right eye. -1 to Ranged Attack."
                               "Go into a coma. You can recover from a coma by making a Con check after 1d6 days, and again after 1d6 weeks if you fail the first check. If you fail both, it is permanent."))

Let’s say we get “Permanently lose 1 Intelligence” for the failed save. Now, working our way back, let’s see what that all evaluates to:

Head Injury; Rolled 12
- +1 Injury
- Concussed for +12 day(s).
- One Fatal Wound.
- Failed to save against cracked skull.  Permanently lose 1 Intelligence

The modified d12 roll resulted in a 12; hence the +12 day(s).


The refactoring that I required was nominal; I needed to account for strings and decided that I’d pass the :roller function the random-table structure instead of the :data of a random-table.

You can find the above random tables, and a lot more in my random-tables-data.el file on Github.

In writing this, I started thinking of a few more features.

First, Invoke the roller with one universal prefix arg (e.g. C-u M-x random-table/roll) to replace all random calls with prompts. That is “Computer asks for a die roll, human provides it, and then evaluates further.”

Second, Overlapping ranges for the :data. The :fetcher would look to find all rows that match. Let’s give an example by re-working the Death and Dismemberment > Physical > Head table:

(random-table/register :name "Death and Dismemberment > Physical > Head"
  :roller #'random-table/roller/death-and-dismemberment/damage
  :private t
  :data '(((1 . 1000) . "Head Injury; Rolled ${current_roll}\n- +1 Injury\n- Concussed for +${current_roll} day(s).")
           ((11 . 1000) . "- +1 Fatal Wound.\n- ${Save vs. Skullcracked}")
           ((16 . 1000) . "- ${current_roll} - 15 Additional Fatal Wounds.\n- ${Save vs. Skullcracked}")))

In the case of rolling the 12, I’d have a list of results:

(list "Head Injury; Rolled ${current_roll}\n- +1 Injury\n- Concussed for +${current_roll} day(s)." "- +1 Fatal Wound.\n- ${Save vs. Skullcracked}")

In other words, when the fetcher returns a list, concatenate that list with a new line (e.g. \n).

I consider it a success that writing this blog post took me longer than refactoring and encoding the Death and Dismemberment mechanics.

-1:-- Further Into My Random Table Emacs Proto Package (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 19, 2023 07:54 PM

Jeremy Friesen: Emacs Proto Package for Random Tables

Summary: A walk-through of a set of Emacs functions that I’ve written to allow for defining and rolling on random tables in my text editor. This involves quite a walk through and quite a bit of emacs-lisp code.


I found a bug in my code and since updated the code and links.

For my solo Role Playing Game (RPG 📖) sessions I write the game play within Emacs 📖 . For each RPG campaign, I’d craft a few custom functions to roll on random tables. I had some common functions that I used, but the table definition was always ad-hoc…until .

Over the last few weeks, I’ve been working on an Emacs proto-package that would normalize tables. At present, my proto-package relies on the dice interpreter of the org-d20 package 📖 . This package represents refactoring the random table tooling I had made for two different games. Black Sword Hack 📖 and The One Ring 📖 .

Describing the Functionality

I type M-x random-table/roll and am prompted to give an Expression. I can select from a pre-populated list of registered tables. Those tables are found in https://github.com/jeremyf/dotemacs/blob/c5a1b98d6e0129cf5af35e8ccf01afc84ac53985/emacs.d/random-tables-data.el Or I can enter an expression, such as “2d6+1”. random-table/roll will then evaluate the expression.

Let’s look at registering a table:

(random-table/register :name "Coin Toss"
                       :data '("Heads" "Tails"))

When I invoke random-table/roll, I can select “Coin Toss”, it will add to the kill ring and write a message based on the roll; either “Heads” or “Tails”. The default reporter kills the value and messages. But you can configure the reporter by setting the random-table/reporter value to a function; see the code for the reporter function.

I could also call random-table/roll and provide the following: “You toss a coin and it lands on ${Coin Toss}”

And will get back: “You toss a coin and it lands on Heads” (assuming you rolled a Heads).

In fact we could register a new table:

(random-table/register :name "Things We Throw"
  :data '("Rocks"
          "Coin and it comes up ${Coin Toss}."))

When we “roll” on that table, when we get the “Coin and…” result, we’ll evaluate rolling on the Coin Toss table. The end result is “Coin and it comes up Heads.”

We can also create ranges, but will need to consider the roller:

(random-table/register :name "Reaction Roll"
  :roller (lambda (&rest args) (+ 2 (random 6) (random 6)))
  :data '(((2) . "Hostile")
           ((3 . 5) . "Unfriendly")
           ((6 . 8) . "Unsure")
           ((9 . 11) . "Amicable")
           ((12) . "Friendly")))

The given :roller is effectively 2d6. And we use the rolled values to then find the correct entry in :data. For example, when we roll a 4 we’d return “Unfriendly”.

The roller can also be a named function; something you can re-use. This is also the place where you could prompt for a modifier or a choice. This bit of code demonstrates how to prompt for different dice to roll.

Let’s look at a more complicated example:

(defun jf/2d6-plus-prompt-for-bonus (&rest args)
  (let ((modifier (read-number "Modifier: " 0)))
    (list (+ 2 modifier (random 6) (random 6)))))

(random-table/register :name "Reaction Roll with Prompt"
    :roller #'jf/2d6-plus-prompt-for-bonus
    :data '(((-1000 . 2) . "Hostile")
             ((3 . 5) . "Unfriendly")
             ((6 . 8) . "Unsure")
             ((9 . 11) . "Amicable")
             ((12 . 2000) . "Friendly")))

In the above case, when we roll the “Reaction Roll with Prompt”, Emacs will prompt for a Modifier. We’ll then use the given modifier to adjust the dice roll. I also adjusted the range to allow for very large numbers being provided by end-users.

As you register tables, via random-table/register, you add them to the table registry. A hash assigned to the random-table/storage/tables variable. The list of tables shown in the M-x random-table/roll can become quite lengthy. To register a table, without adding it to the selection list, add :private t as one of the key word arguments.

Below is the “Name” table. When we roll on the “Name” table we’ll pick a random one. Then roll on a “sub-table”. So as to not clutter the list, we mark those “sub-tables” as :private t.

(random-table/register :name "Name"
    :data '("${Name > Male}" "${Name > Female}" "${Name > Non-Binary}"))

(random-table/register :name "Name > Male"
    :private t
    :data '("George" "Michael"))

(random-table/register :name "Name > Female"
    :private t
    :data '("Mary" "Margaret"))

(random-table/register :name "Name > Non-Binary"
    :private t
    :data '("Quin" "Ash"))

Given the composition of tables, we may also want to store the results of the roll for future reference. Why might we do this? Here’s a concrete example of rolling a set of dice, then looking up the results of those dice on two separate tables. Some tables may say “Roll 3 dice. Then on table one use the highest value. And on table two use the lowest value. And on table three, if there are doubles, use the number that is the “double”.

(random-table/register :name "High Low"
  :roller (lambda (&rest args) (list (+ 1 (random 6)) (+ 1 (random 6))))
  ;; We include this so that we only return the first data element.  The
  ;; dice rolls are for the High Value and Low Value
  :fetcher (lambda (data roll) (car data))
  :data '("\n- High :: ${High Value}\n- Low :: ${Low Value}")
  :store t)

(random-table/register :name "High Value"
  :reuse "High Low"
  :private t
  :filter #'max
  :data '("One" "Two" "Three" "Four" "Five" "Six"))

(random-table/register :name "Low Value"
  :reuse "High Low"
  :private t
  :filter #'min
  :data '("One" "Two" "Three" "Four" "Five" "Six"))

As of I store the roll in a somewhat naive manner; for a table with :store t, when we “roll on that table” we add to a hash the table name and the results of the roll (e.g. the specific dice as a list). Then until we’ve fully evaluated the roll for that table, we can reference the dice results for that table.

One thing I introduced in the above was the :fetcher and :filter elements. The :filter takes the dice pool (as a list) and returns an integer. The :fetcher takes the integer and looks things up in the provided :data.

The general flow is:

  • :roll the dice
  • :filter the roll
  • :fetch the filtered result

That flow is defined in random-table/evaluate/table. See definition of random-table/evaluate/table.


Emacs random-table package code
;;; random-table --- Roll on some tables. -*- lexical-binding: t -*-

;; Copyright (C) 2023 Jeremy Friesen
;; Author: Jeremy Friesen <jeremy@jeremyfriesen.com>

;; This file is NOT part of GNU Emacs.

;;; Commentary:

;; This package provides a means of registering random tables (see
;; `random-table' and `random-table/register') and then rolling on those tables
;; (see the `random-table/roll').
;; The `random-table/roll' is an `interactive' function that will prompt you to
;; select an expression.  You can choose from a list of registered public tables
;; or provide your own text.  This package uses the `s-format' to parse the
;; given expression.
;; The guts of the logic is `random-table/evaluate/table' namely how we:
;; - Gather the dice (represented by the :roller)
;; - Filter the dice to a value (represented by the :filter), we might pick a
;;          single dice rolled or sum them or whatever.
;; - Fetch the filtered result from the table.
;; - Evaluate the row.
;; Examples:
;; - (random-table/roll "2d6") will roll 2 six-sided dice.
;; - (random-table/roll "There are ${2d6} orcs.") will roll 2 six-sided dice and
;;   output the sentence "There are 7 orcs."
;; Tables can reference other tables, using the above string interpolation
;; (e.g. "Roll on ${Your Table}" where "Your Table" is the name of a registered
;; table.).  No considerations have been made to check for cyclical references,
;; you my dear human reader, must account for that.

;;; Code:

;;;; Requirements:
(require 'org-d20)
(require 'cl)

;;;; Data Structures and Storage and Defaults
(cl-defstruct random-table
  "The definition of a structured random table.

I roll the dice, filter the results, and fetch from the table.
The `random-table/evaluate/table' defines the steps we take to
\"roll on the table.\"

The slots are:

- :name :: the human readable and reference-able name (used for
  completing read and the key for the table storage).
- :data :: the tabular data, often as a list of strings.  By
  design, those list of strings can have interpolation
 (e.g. \"${2d6}\" both of dice structures but also of other
- :roller :: function to roll dice and return list of dice results.
- :filter :: function to filter the list of dice.
- :fetcher :: function that takes two positional arguments (see
  `random-table/fetcher/default'.); it is used to fetch the correct entry
  from the table.
- :private :: when true, do not show in list of rollable tables.
- :store :: When non-nil, we store the roller's value for the
  duration of the table evaluation.  Useful for when you have one
  roll that you use for multiple tables.
- :reuse :: the :name of a table's stored dice results.

About :reuse and :store

There are cases where we want to use one set of dice roles.  For
example, in the \"Oracle (Black Sword Hack)\" table we roll dice
and use those dice results to determine both the answer as well
as whether there are unexpected events.  All from the same roll."
  (roller #'random-table/roller/default)
  (filter #'random-table/filter/default)
  (fetcher #'random-table/fetcher/default)
  (private nil)
  (store nil)
  (reuse nil))

(cl-defun random-table/register (&rest kws &key name data &allow-other-keys)
  "Store the DATA, NAME, and all given KWS in a `random-table'."
  (let* ((key (intern name))
          (struct (apply #'make-random-table :name key :data (-list data) kws)))
    (puthash key struct random-table/storage/tables)))

(defvar random-table/storage/results
  "An ephemeral storage for dice results of rolling for a table.

As part of the rolling, we both add to and remove those stored
values; that is to say functions are responsible for clean-up.
See `random-table' for discussion about storage and reuse.")

(defvar random-table/storage/tables
  "A hash-table of random tables.

The hash key is the \"human readable\" name of the table (as a symbol).
The hash value is the contents of the table.")

(defun random-table/roller/default (&rest data)
  "Return list with one element that is between 1 the `length' of the given DATA.

Rollers should return a `list' of values; perhaps the results of
 individual dice rolled.

See `random-table/filter/default'."
  ;; Constant off by one errors are likely
  (list (+ 1 (random (length (-list data))))))

(defun random-table/filter/default (&rest rolls)
  "Filter the given ROLLS and return an integer.

See `random-table/roller/default'."
  (apply #'+ (-list rolls)))

(defun random-table/fetcher/default (data &optional roll)
  "Find ROLL on the given table's DATA.

When ROLL is not given, choose a random element from the TABLE."
  (if-let ((index (if (integerp roll) roll (car roll))))
    ;; Sniff out if the first element to see if we're dealing with a table that has ranges.
    (if (-cons-pair? (car data))
      ;; We have a cons-pair, meaning we have multiple rolls mapping to the same
      ;; result.
      (cdr (seq-find
             (lambda (row)
               (if (-cons-pair? row)
                 (let ((range (car row)))
                     ((-cons-pair? range)
                       (and (>= index (car range)) (<= index (cdr range))))
                     ((listp range)
                       (member index range))
                     ((integerp range) (= index range))
                       (error "Expected `cons', `list', or `integer' got %s for row %S." (type-of range) row))))
                 (member index (car row))))
      ;; Off by one errors are so very real.
      (nth (- index 1) data))
    (seq-random-elt data)))

(defvar random-table/reporter
  "The function takes two positional parameters:

- EXPRESSION :: The text to evaluate for \"rolling\"
- RESULT :: The results of those rolls.

See `random-table/reporter/as-kill-and-message'.")

(defun random-table/reporter/as-kill-and-message (expression result)
  "Responsible for reporting the EXPRESSION and RESULT.

See `random-table/reporter'."
  (let ((text (format "%s :: %s" expression result)))
    (kill-new text)
    (message text)))

;;;; Interactive
(global-set-key (kbd "H-r") #'random-table/roll)
(defun random-table/roll (text)
  "Evaluate the given TEXT by \"rolling\" it.

This can either be a named table or a general text (e.g. 2d6).

Or a combination of multiple tables.

We report that function via `#'random-table/reporter'."
  (interactive (list (completing-read "Expression: "
                       ;; Predicate that filters out non-private tables.
                       (lambda (name table &rest args) (not (random-table-private table))))))
  ;; TODO: Consider allowing custom reporter as a function.  We already register
  ;; it in the general case.
  (apply random-table/reporter
    (list text (random-table/roll/parse-text text))))

(defun random-table/roll/parse-text (text)
  "Roll the given TEXT.

Either by evaluating as a `random-table' or via `s-format'."
  (if-let* ((table (random-table/get-table text :allow_nil t)))
    (random-table/evaluate/table table)
    ;; We have specified a non-table; roll the text.  We'll treat a non-escaped on as a dice text.
      (s-format (if (string-match-p "\\${" text) text (concat "${" text "}"))

(defun random-table/roll/parse-text/replacer (text)
  "Roll the TEXT; either from a table or as a dice-expression.

This is constructed as the replacer function of `s-format'."
  (if-let ((table (random-table/get-table text :allow_nil t)))
    (random-table/evaluate/table table)
    ;; Ensure that we have a dice expression
    (if (string-match-p "[0-9]?d[0-9]" text)
      (format "%s" (cdr (org-d20--roll text)))

(defun random-table/evaluate/table (table)
  "Evaluate the random TABLE.

See `random-table'."
  (let* ((data (random-table-data table))
          (name (random-table-name table))
          (rolled (random-table/evaluate/table/roll table))
          (filtered (apply (random-table-filter table) (-list rolled)))
          (row (if filtered (apply (random-table-fetcher table) (list data (-list filtered)))
          (results (or (when row (random-table/roll/parse-text row)) "")))
    (remhash (random-table-name table) random-table/storage/results)

(defun random-table/evaluate/table/roll (table)
  "Roll on the TABLE, favoring re-using and caching values.

Why cache values?  Some tables you roll one set of dice and then
use those dice to lookup on other tables."
  (let ((results
          (or (when-let ((reuse-table-name (random-table-reuse table)))
                  (gethash (intern reuse-table-name) random-table/storage/results)
                    (random-table/get-table reuse-table-name))))
            (apply (random-table-roller table) (-list (random-table-data table))))))
    (when-let ((stored-table-name (random-table-store table)))
      (puthash (random-table-name table) results random-table/storage/results))

(cl-defun random-table/get-table (value &key allow_nil)
  "Coerce the given VALUE to a registered `random-table'.

When the given VALUE cannot be found in the
`random-table/stroage/tables' registry we look to ALLOW_NIL.

When ALLOW_NIL is non-nil, we return `nil' when no table is found
in `random-table/stroage/tables' registry.

When ALLOW_NIL is `nil' we raise an `error' when no table was
found in the `random-table/stroage/tables' registry."
  (if-let ((table (cond
                    ((random-table-p value)
                    ((symbolp value)
                      (gethash value random-table/storage/tables))
                    ((stringp value)
                      (gethash (intern value) random-table/storage/tables))
                    ((integerp value)
                      (error "Expected %s to be a `random-table', `symbol', `integer', or `string' got %s."
                        (type-of value))))))
    (unless allow_nil
      (error "Could not find table %s; use `random-table/register'." value))))

(defun random-table/dice/parse-spec (spec)
  "Convert SPEC to list:

   - Number of dice
   - Face
   - Adder

  e.g. \"1d6\" -> (1 6 0) or \"2d10+2\" -> (2 10 2) or \"4dF\" -> (4 \"f\" 0)"
  (when (string-match
    (list (random-table/dice/string-to-number (match-string 1 spec) 1)
          (random-table/dice/string-to-number (match-string 2 spec)
          (random-table/dice/string-to-number (match-string 3 spec) 0))))

(defun random-table/dice/string-to-number (spec default)
  (let ((n (if (stringp spec)
               (string-to-number spec)
    (cond ((null spec) default)
          ((> n 0) n)
          ((string= "+" spec) 0)
          ((string= "-" spec) 0)
      (t spec))))

;; (let ((spec (random-table/dice/parse-spec spec-string)))
;;   (apply 'decide-roll-dice-spec (if spec spec '(1 6 0))))

(provide 'random-table)
;;; random-table.el ends here


As I write up this blog post, I’m starting to think of how I might extend this. I certainly would love to move away from the dependency on the org-d20 package for dice rolling. However, it has thus far worked in a pinch.

I’ve also used this as a bit of a code kata. In my mentoring software developers, I often encourage folks to explore writing code for their hobby/hobbies. The idea being that you are likely a suitable domain expert in your hobby and understand the complexities. From that point of knowledge you can explore writing code.

And in this case, I wanted to explore writing more Lisp; partly because I wanted these random tables available in Emacs during my solo RPG sessions. But also to delve into a few more concepts Lisp; namely hashes and structs.

I had considered the decide-mode package but a major constraint was the somewhat tricky Black Sword Hack ’s “The Dark God’s Oracle.” The procedure is to ask a yes/no question and choose it’s likelihood:

“Don’t think so”
3d6, keep lowest
2d6, keep lower
“Who knows?”
2d6, keep higher
3d6, keep highest

From the dice pool you look up the answer to the yes/no. You also check the roll…if there are doubles on the dice, you have a kicker. That bit of “dice” memory was not something I saw, nor wanted to incorporate in another package. Perhaps there’s an algebra/calculus to express this. But for now, I’ve encoded that logic as lambdas.

-1:-- Emacs Proto Package for Random Tables (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 17, 2023 04:13 AM

Jeremy Friesen: Emacs Functions to Navigate Ruby Files by Method Comments

Summary: Questions about idiomatic code lead to thinking about how I might use that idiom for navigation; which meant “Time to hack on Emacs” and write up some functions for navigation.


while pairing with a team member, she asked about an idiom she observed. Namely that I wrote code comments as follows:

The first line of the comment block had two # characters (e.g. ##) and then following commented lines begin with #. Here’s an example:

# Describe the method
# @param value [Object]
# @return [String]
def call(value)

I told my team member that I had picked this idiom up from another Samvera 📖 developer. I find it aesthetically pleasing. A nice little “cap” to a comment block, gently saying that lines above this one are not part of this chunk.

While talking, I started wondering if that other Samvera developer, also an Emacs 📖 user, had a key-binding and functions for navigating between these items?

I could see the utility of positioning the screen so that the first line is the start of a comment block. Namely, I could see the inline documentation and likely the entire method definition.

So, I wrote up two interactive functions that I bind in the ruby-mode-map. I also bind them in the ruby-ts-mode-map, but that binding is done just a bit differently, due to load sequence.


The first is a variable definition for the regular expression:

(defvar jf/ruby-mode/comment-header-regexp
  "The regular expression for a Ruby comment header.

I noticed that a fellow Rubyist would start her method comment
blocks with \"##\".  I liked how that looked, creating a clear
marker of a comment block.

Then when pairing with a team member she asked about those
comment two \"##\".  I said my assumption was that they were used
for navigation.  It would make sense as an anchoring point for
positioning the cursor.")

Then I define the move forward function. It checks if I’m already on the starting line of a comment; if so it moves forward one line then searches for the next line who’s first two non-space characters are ##. Finally it positions the cursor to the beginning of that line, repositions the screen so that this first line is the first line displayed in the window. Then pulses to show where the cursor is at.

When no matches are found it moves the cursor to the bottom of the buffer.

(defun jf/ruby-mode/commend-header-forward ()
  "Move to next line matching `jf/ruby-mode/comment-header-regexp'."
  (when (string-match-p
  (condition-case err
      (search-forward-regexp jf/ruby-mode/comment-header-regexp)
      (recenter scroll-margin t)
    (error (goto-char (point-max)))))

This is similar to the above function, but instead searches backwards.

(defun jf/ruby-mode/comment-header-backward ()
  (when (string-match-p
  (condition-case err
      (search-backward-regexp jf/ruby-mode/comment-header-regexp)
      (recenter scroll-margin t)
    (error (goto-char (point-min)))))

Last I bind keys as follows.

(define-key ruby-mode-map (kbd "s-ESC") #'jf/ruby-mode/comment-header-backward)
(define-key ruby-mode-map (kbd "C-s-]") #'jf/ruby-mode/commend-header-forward)

It looks a little odd, but s-[ default maps to Escape, so on my machine I have the following:

  • Ctrl + Cmd + [ moves backward
  • Ctrl + Cmd + ] moves forward

My brain has long mapped the [ and ] with modifiers as navigation keys.

I had considered using interactive search (e.g. isearch-forward) but that is something I haven’t used as much and the regular expression that I defined is not something I’d want to regularly type; though in a pinch searching for ## would be reasonable.


I love pairing with folks who ask questions; ones that bring curiosity to the myriad of assumptions, approaches, and idioms. From those questions grows a greater awareness and shared context. Which, I find, improves the overall experience of software development.

-1:-- Emacs Functions to Navigate Ruby Files by Method Comments (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 15, 2023 04:12 PM

Jeremy Friesen: Implementing Changes to Org-Mode Time Tracking

Summary: A walk through of my functional adjustments to my time tracking in Org Mode. These adjustments leverage functions that I had already developed and align with other existing processes.


As I wrote in Revisiting My Org-Mode Time Tracking and Laying out a Different Direction, I’m pivoting my time tracking. I want to keep notes in corresponding projects. This should improve the conditions for information transfer when joining and leaving projects. The transfer can be me or others joining the project.

In my workflow I had three primary time tracking functions:

  • Creating a task
  • Generating a stand-up report
  • Summary of hours per project with each task clocked that day



First, I want get the filenames of all projects. My convention is adding an Org-Mode 📖 keyword of PROJECT_PATHS to the document. Using Ripgrep 📖 , jf/git-project-paths/dynamic finds those files.

(defun jf/git-project-paths/dynamic ()
  "Return a list of code repository paths."
          "rg \"^#\\+PROJECT_PATHS: +[^\\.]+\\. +\\\"(~/git/[^/]+/)\\\"\\)\" "
          "~/git/org --no-ignore-vcs --replace='$1' "
          "--only-matching --no-filename")))

Using jf/git-project-paths/dynamic I then append additional files (via jf/org-mode/agenda-files). These are my pre-existing agenda files. As time passes, I’ll slowly move content from those files and eventually remove the conditionals.

(defun jf/org-mode/agenda-files ()
  "Return a list of note files containing 'agenda' tag.

Uses the fd command (see https://github.com/sharkdp/fd)

We want files to have the 'projects' `denote' keyword."
  (let ((projects (mapcar (lambda (el) (cdr el))
    (when (file-exists-p jf/agenda-filename/scientist)
      (setq projects (cons jf/agenda-filename/scientist projects)))
    (when (file-exists-p jf/agenda-filename/personal)
      (setq projects (cons jf/agenda-filename/personal projects)))

I dynamically update the org-agenda-files with a few different hooks. This ensures consistently updated agenda files.

(defun jf/org-mode/agenda-files-update (&rest _)
  "Update the value of `org-agenda-files'."
  (setq org-agenda-files (jf/org-mode/agenda-files)))
(advice-add 'org-agenda :before #'jf/org-mode/agenda-files-update)
(advice-add 'org-todo-list :before #'jf/org-mode/agenda-files-update)
(add-hook 'after-init-hook #'jf/org-mode/agenda-files-update)

Creating a Task

I created a new capture “Task (for project)” template as follows:

(add-to-list 'org-capture-templates
             '("t" "Task (for Project)"
               plain (function jf/org-mode/capture/project-task/find)
               :empty-lines-before 1
               :empty-lines-after 1
               :clock-in t
               :jump-to-capture t))

The above capture template leverages the jf/org-mode/capture/project-task/find function, which:

  1. Prompts for a project (via jf/project/list-projects).
  2. Then prompts for an existing task within that project (via jf/org-mode/existing-tasks).
  3. And starts the clock for that task.

When the task does not already exist, it creates a top-level heading at the bottom of the file. In writing this, I realize when there is a “Footnotes” headline, I should position just above that location.

The end result is positioning point to the correct spot in the chosen project’s associated file.

(defun jf/org-mode/capture/project-task/find ()
  "Find the project file and position to the selected task."
  (let* ((project (completing-read "Project: " (jf/project/list-projects)))
          (filename (cdar (jf/project/list-projects :project project)))
          (tasks (jf/org-mode/existing-tasks filename))
          (task-name (completing-read (format "Task for %s: " project) tasks)))
    ;; Defer finding this file as long as possible.
    (find-file filename)
    (if-let (task (alist-get task-name tasks nil nil #'string=))
      (goto-char (org-element-property :contents-end task))
        (goto-char (point-max))
        ;; Yes make this a top-level element.  It is easy to demote and move
        ;; around.
        (insert "* TODO " task-name " :tasks:\n\n")))))

The jf/project/list-projects function creates an alist with the car of the name of the project and the cdr as the filename for the project. In writing this, I realize that I should refactor as there’s duplicate behavior. But for now let’s proceed.

Having this alist structure makes the prompting for completing-read very simple.

(cl-defun jf/project/list-projects (&key (project ".+")
                                         (directory org-directory))
  "Return a list of `cons' that match the given PROJECT.

The `car' of the `cons' is the project (e.g. \"Take on Rules\").
The `cdr' is the fully qualified path to that projects notes file.

The DIRECTORY defaults to `org-directory' but you can specify otherwise."
  (mapcar (lambda (line)
                  (let* ((slugs (s-split ":" line))
                                (proj (s-trim (car (cdr slugs))))
                                (filename (s-trim (car slugs))))
                    (cons proj filename)))
                "rg \"^#\\+PROJECT_NAME: +(" project ") *$\" " directory
                " --only-matching --no-ignore-vcs --with-filename -r '$1' "
                "| tr '\n' '@'"))

The jf/org-mode/existing-tasks function leverages the Org Element API to search the file for all of the tasks.

I provide a filename, then search for headlines that are marked as “todo” or one tagged with “tasks.”

(defun jf/org-mode/existing-tasks (&optional filename)
  "Return an alist of existing tasks in given FILENAME.

Each member's `car' is title and `cdr' is `org-mode' element.

Members of the sequence either have a tag 'tasks' or are in a todo state."
  (with-current-buffer (or (and filename
                                (find-file-noselect filename))
    (mapcar (lambda (headline)
              (cons (org-element-property :title headline) headline))
                (org-element-parse-buffer 'headline)
              (lambda (headline)
                   (org-element-property :todo-type headline) 'todo)
                  (member "tasks"
                          (org-element-property :tags headline)))

Generating a Stand-up Report

My previous function relied on the time tracking hierarchy of Year > Month > Day > Project > Task. With my new structure, the previous function would not work.

One pathway, I started pursuing was using the org-ql package 📖 . I have the following:

(use-package org-ql
  :straight t)

(cl-defun jf/org-mode/clocked-grouped-by-title (&key (from 0) (to 0))
  (-group-by (lambda (row) (car row))
              :from #'org-agenda-files
              :select '(list (org-get-title)
                              (org-get-heading t t)))
              :where '(clocked :from from :to to)
              :order-by (lambda (a b)
                          (string= (car a) (car b))))))

It works to get a list that I can further act on. However, I ended up deferring implementing this because the summary of hours function for each week works quite well.

Summary of Hours

Built into Org-Mode is the clocktable. The introduction of the documentation is as follows: “Org mode can produce quite complex reports based on the time clocking information.”

I Software Services by Scientist.com 📖 that has the following Org mode dynamic block:

#+BEGIN: clocktable :scope agenda :tcolumns 1 :link t :block thisweek :narrow 80! :step day :stepskip0 t :fileskip0 t :filetitle t

Below is an example of generating the clocktable.

Daily report: [2023-07-28 Fri]
| File  | Headline                    | Time |
|       | ALL Total time              | 0:45 |
| Emacs | File time                   | 0:45 |
|       | Refactoring Time Tracking   | 0:45 |

Daily report: [2023-07-29 Sat]
| File  | Headline                    | Time |
|       | ALL Total time              | 1:45 |
| Emacs | File time                   | 1:45 |
|       | Refactoring Time Tracking   | 1:00 |
|       | Watch Macros and Prompting  | 0:45 |

With the above clocktable parameters, each headline (e.g. “Refactoring Time Tracking” or “Watch Macros and Prompting”) is a link. And the clocktable skips all headlines that doesn’t have clocked time for the “Daily Report.”

And seeing this output, that summary is adequate for reminding me of my daily stand-up. From that, I can contextualize.


In Revisiting My Org-Mode Time Tracking and Laying out a Different Direction, I spent time thinking about the situation. After that reflection, I spent time coding changes. And these changes were quick to create and adopt as they spliced into another aspect of my workflow.

In other words, these changes better serve my thinking and note-taking, while providing adequate means of reporting my days work and recording my week’s effort.

On I’m switching teams at Software Services by Scientist.com . This seems like a good time to change my time-tracking and note-taking processes.

-1:-- Implementing Changes to Org-Mode Time Tracking (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--July 30, 2023 06:52 PM

Jeremy Friesen: Revisiting My Org-Mode Time Tracking and Laying out a Different Direction

Summary: Working through my existing note-taking and time-tracking approaches and outlining a new direction; favoring thinking and task cohesion over easier time-reporting.

Revisiting My Previous Steps

at Software Services by Scientist.com 📖 I have been recording my time. In Org Mode Capture Templates and Time Tracking I wrote about my approach.

To sum up, I was using a single agenda file for all of my job’s time tracking. The hierarchy was: Year > Month > Day > Project > Task. When I started a task for a project, I would add a node for the project beneath the given day, then I’d add a task node beneath that project node. I’d track time to that task.

I explored Adding a Function to Carry Forward an Org-Mode Agenda Item; because I found a need to send forward the notes from one day to another.

Along the way, I wrote Custom Org-Mode Capture Function for Annotating Bad Code. I had begun capturing content to more than just my projects. In particular I was capturing towards the active Org-Mode 📖 clock.

Then I started working on longer running tasks; ones that took days or weeks to finish. They often involved significant note taking to think through the functions or document the steps to reproduce. This meant I started leveraging my Project Dispatch Menu with Org Mode Metadata, Denote, and Transient.

Playing to Find Out

On , I wrote and implemented Dynamic Org Agenda List Based on Denote Keywords. I was already using a dynamic function for org-agenda-files; but it was serving a different function. Which got me thinking about my current practices and functionality. Namely that my single agenda file, with it’s Year > Month > Day > Project > Task hierarchy, was a structure that mirrored the time reporting process and schedule. My workflow was in service to that.

However in playing with a more dynamic setting of the org-agenda-files list, I became curious about how I might revisit this approach. It felt right to:

  • Move my time tracking into projects.
  • Keep longer running notes coherent from day to day.
  • Have my note taking for my job serve thinking and knowledge sharing instead of time reporting constraints.

, I did further reading into Org-Mode’s clock table. And started adjusting the reporting header that I use. Later , while working (and sweating) at the Elkhart County 4-H Fair 📖 , I spent more time writing about my desired workflow.

Writing Down Some Goals

My focal goal is to improve the cohesion of my note-taking (and thus thinking). The key way to do that is to stop carrying notes forward from day to day. Instead write those notes to a more canonical location (e.g. a task within a project file).

I want to have the following specific reporting features:

  • An overview of how many hours I’ve logged for each day of the week
  • A means to generate my daily stand-up
  • A means to easily record my hours and brief notes in our time tracking software

I want to have the means to relatively quickly create a new project. I also want a means to continue to quickly start a task by selecting the applicable project.

The function that I most want is as follows: when I start a task, I want to be able to first select an existing project, then either create a new task or select an existing non-completed task on the chosen project. I want to jump to that task.

I like tracking time at the task level, because this helps me generate the daily stand-up as well as provide notes for each day’s recorded time entry.


I had been using a process for quite awhile; it worked for reporting time but was not working as well for thinking and knowledge sharing. I had intuited this for some time, but it was the intersection of playing with even more dynamic org-agenda-files and adjusting my clocktable parameters that I thought through a different approach.

In the coming days, I’ll refactor some functions, introduce different ones, and again shift my workflow. And Org-Mode and Emacs 📖 will continue with me on this journey; themselves flexible and ready to help me in self-discovery.

-1:-- Revisiting My Org-Mode Time Tracking and Laying out a Different Direction (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--July 29, 2023 01:37 PM

Jeremy Friesen: Dynamic Org Agenda List Based on Denote Keywords

Porting some Functions for Org-Roam to Denote

I read Boris Buliga’s “Task management with org-roam Vol. 5: Dynamic and fast agenda” post.

Boris provide code and explanation to automatically updating the org-agenda-files list with only the files that have a project tag. The org-agenda-files variable defines the files that Org-Mode 📖 uses to drive it’s agenda feature set. Show todos, run time reports, etc.

If the number of org-agenda-files becomes too large, then it begins to impact performance of the agenda feature set. Thus keeping a pruned list helps with performance.

I really like the idea and appreciate the implementation. However, I don’t use Org-Roam 📖 ; instead I used Denote 📖 . So I spent a bit of time mapping Boris’s code to my reality.

Below is my walk through.

First, I am going to use a different keyword than “project.” I’m favoring the explicit “agenda”. When the note has that keyword it is part of the agenda.

(defvar jf/org-mode/agenda-keyword
  "The `denote' keyword that identifies a note as part of `org-mode' agenda.")

Next up is almost a direct copy of vulpea-project-p; it returns non-nil when there’s a “todo” keyword on any of the nodes.

(defun jf/org-mode/agenda-p ()
  "Return non-nil if current buffer has any todo entry.

TODO entries marked as done are ignored, meaning the this
function returns nil if current buffer contains only completed

From https://d12frosted.io/posts/2021-01-16-task-management-with-roam-vol5.html"
  (when (derived-mode-p 'org-mode)
     (org-element-parse-buffer 'headline)
     (lambda (h)
       (eq (org-element-property :todo-type h)
     nil 'first-match)))

This is an echo of Boris’s vulpea-project-update-tag function. It’s interweaves with the functions used in Denote to determine a file’s keywords. Aspects of the when-let* function could be compressed into a native Denote function.

(add-hook 'before-save-hook
(add-hook 'find-file-hook

(defun jf/org-mode/denote-update-project-update-tag ()
  "Update `jf/org-mode/agenda-keyword' tag in the current buffer."
  (when-let* ((_proceed (not (active-minibuffer-window)))
              (file (buffer-file-name))
              (_proceed (denote-file-is-note-p file))
              (file-type (denote-filetype-heuristics file))
              (new-keywords (denote-retrieve-keywords-value
              (keywords new-keywords))
      (goto-char (point-min))
      (if (jf/org-mode/agenda-p)
          (setq new-keywords (cons
        (setq new-keywords (remove

      ;; cleanup duplicates
      (setq new-keywords (seq-uniq new-keywords))

      ;; update tags if changed
      (when (or (seq-difference keywords new-keywords)
                (seq-difference new-keywords keywords))
        (message "Adjusting \"%s\" keyword for %s"
                 jf/org-mode/agenda-keyword file)
        (denote-rewrite-keywords file new-keywords file-type)t))))

Where Org-Roam uses SQLite for storying and accessing metadata (e.g. the tags/keywords), Denote opts instead for front-matter and file name conventions. What I have below uses the fd 📖 program to query the file system for the tags/keywords.

(defvar jf/org-mode/directory-for-agendas

(defun jf/org-mode/agenda-files ()
  "Return a list of note files containing 'agenda' tag.

Uses the fd command (see https://github.com/sharkdp/fd)

We want files either begin with the `jf/org-mode/agenda-keyword'
 or by `denote' conventions have the keyword.  Hence the complex regular
  (let ((default-directory (file-truename
    (s-split "\n"
               (concat "fd --no-ignore --absolute-path --extension org "
                       "'(^|_)" jf/org-mode/agenda-keyword "[_\\.]'"))))))

Next is similar code to update the org-agenda-files based on their tags.

(defun jf/org-mode/agenda-files-update (&rest _)
  "Update the value of `org-agenda-files'."
  (setq org-agenda-files (jf/org-mode/agenda-files)))
(advice-add 'org-agenda :before #'jf/org-mode/agenda-files-update)
(advice-add 'org-todo-list :before #'jf/org-mode/agenda-files-update)

Last is the most significant change. Because I’m relying on the file name to encode the keywords, I need to ensure some synchronization. In my experimentation, I ran into problems trying to rename the file during save. With this change, I rename the file when I close/kill it.

(defun jf/org-mode/kill-buffer-hook ()
    (when-let* ((_proceed (not (active-minibuffer-window)))
                (file (buffer-file-name))
                (_proceed (denote-file-is-note-p file)))
      (call-interactively #'denote-rename-file-using-front-matter file)))
  (add-hook 'kill-buffer-hook #'jf/org-mode/kill-buffer-hook)

I’m rolling this into my workflow; I can see this drifting into the space of a right and proper package. Let me know if there’s interest.

-1:-- Dynamic Org Agenda List Based on Denote Keywords (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--July 28, 2023 06:41 PM

Jeremy Friesen: Unfurling of Issue Reference Abbreviations in Github and other Git Forge Issues

Making it Easier to Minimize Risk of Information Loss

In Emacs, when I’m writing a commit message for a repository backed by version control, and I type #123 the bug-reference package overlays that #123 with links to the remote issue. I can then “click” on #123 and jump to the issue at the remote repository; a convenient feature!

However that convenience comes with a cost, namely those terse references do two things:

  • create low-level lock-in
  • increase the risk of information loss

If I were to change the host of that repository or transfer ownership, the #123 becomes disconnected from what it once referenced.

I prefer, instead, to use full URLs 📖 . This way there is no ambiguity about what I’m referencing. Unless of course the remote service breaks links or goes away.

Adding further nuance, due to the nature of my work, I’m often referencing other repository’s issues and pull requests during the writing of a commit.

Enter Automation

I’ve been playing with Completion at Point Functions (CaPFs 📖) and decided the explore automatically creating those URLs . See Completion at Point Function (CAPF) for Org-Mode Links. The end goal is to have a full URL. For example https://github.com/samvera/hyrax/issues/6056.

I broke this into two steps:

  • Create a CaPF for finding the project
  • Create a CaPF for replacing the project and issue with the URL

I settled on the following feature:

Given that I have typed “/hyr”
When I then type {{{kbd(TAB)}}}
Then auto-complete options should include “/hyrax”

I went a step further in my implementation, when I select the project completion candidate I append a # to that. I end up with /hyrax# and the cursor is right after the # character. From which I then have my second CaPF .

Given that the text before <point> is "/hyrax#123"
When I type {{{kbd(TAB)}}}
Then auto-complete will convert "/hyrax#123"
     to "https://github.com/samvera/hyrax/issues/123"


Create a CaPF for finding the project

First let’s look at the part for finding a project. I do this via jf/version-control/project-capf.

(defun jf/version-control/project-capf ()
  "Complete project links."
  ;; While I'm going to replace "/project" I want to make
  ;; sure that I don't have any odd hits (for example
  ;; "/path/to/file")
  (when (looking-back "[^[:word:]]/[[:word:][:digit:]_\-]+"
    (let ((right (point))
           (left (save-excursion
                     ;; First check for the project
                    (jf/capf-max-bounds) t)
      (list left right
        (lambda (text _status)
          (delete-char (- (length text)))
          (insert text "#"))
        :exclusive 'no))))

The above function looks backwards from point, using jf/capf-max-bounds as the bounds of how far back to look. If there’s a match the function then gets the left and right boundaries and calls jf/version-control/known-project-names to get a list of all possible projects that I have on my machine.

The jf/capf-max-bounds function ensures that we don’t attempt to look at a position outside of the buffer. See the below definition:

(cl-defun jf/capf-max-bounds (&key (window-size 40))
  "Return the max bounds for `point' based on given WINDOW-SIZE."
  (let ((boundary (- (point) window-size)))
    (if (> 0 boundary) (point-min) boundary)))

The jf/version-control/known-project-names leverages the projectile package to provides a list of known projects. I’ve been working at moving away from projectile but the projectile-known-projects variable just works, so I’m continuing my dependency on projectile. I want to migrate towards the built-in project package, but there are a few points that I haven’t resolved.

(cl-defun jf/version-control/known-project-names (&key (prefix "/"))
  "Return a list of project, prepending PREFIX to each."
  (mapcar (lambda (proj)
            (concat prefix (f-base proj)))

I then add jf/version-control/project-capf to the completion-at-point-functions variable. I also need to incorporate that elsewhere, based on various modes. But that’s a different exercise.

(add-to-list 'completion-at-point-functions #'jf/version-control/project-capf)

The above code delivers on the first feature; namely auto completion for projects that sets me up to deliver on the second feature.

Create a CaPF for replacing the project and issue with the URL

The jf/version-control/issue-capf function below builds on jf/version-control/project-capf convention, working then from having an issue number appended to the text.

(defun jf/version-control/issue-capf ()
  "Complete project issue links."
  ;; While I'm going to replace "/project" I want to make sure that I don't
  ;; have any odd hits (for example /path/to/file)
  (when (looking-back "[^[:word:]]/[[:word:][:digit:]_\-]+#[[:digit:]]+"
    (let ((right (point))
           (left (save-excursion
                     (jf/capf-max-bounds) t)
      (list left right
        :exclusive 'no))))

I continue to leverage jf/capf-max-bounds querying for all matching version control text within the buffer (via jf/version-control/text):

(defun jf/version-control/text ()
  "Find all matches for project and issue."
  (s-match-strings-all "/[[:word:][:digit:]_\-]+#[[:digit:]]+" (buffer-string)))

Once we have a match, I use jf/version-control/unfurl-issue-to-url to convert the text into a URL . I had originally tried to get #123 to automatically unfurl the issue URL for the current project. But I set that aside as it wasn’t quite working.

(defun jf/version-control/unfurl-issue-to-url (text _status)
  "Unfurl the given TEXT to a URL.

Ignoring _STATUS."
  (delete-char (- (length text)))
  (let* ((parts (s-split "#" text))
          (issue (cadr parts))
          (project (or (car parts) (cdr (project-current)))))
    (insert (format
              (jf/version-control/unfurl-project-as-issue-url-template project)

That function relies on jf/version-control/unfurl-project-as-issue-url-template which takes a project and determines the correct template for the project.

(cl-defun jf/version-control/unfurl-project-as-issue-url-template (project &key (prefix "/"))
  "Return the issue URL template for the given PROJECT.

Use the provided PREFIX to help compare against
  (let* ((project-path
          (car (seq-filter
                (lambda (el)
                   (s-ends-with? (concat project prefix) el)
                   (s-ends-with? project el)))
          (s-trim (shell-command-to-string
                    "cd %s && git remote get-url origin"
    (s-replace ".git" "/issues/%s" remote)))

And last, I add jf/version-control/issue-capf to my list of completion-at-point-functions.

(add-to-list 'completion-at-point-functions #'jf/version-control/issue-capf)


While demonstrating these functions to a co-worker, I said the following:

“The purpose of these URL unfurling functions is to make it easier to minimize the risk of losing information that might be helpful in understanding how we got here.”

In other words, information is scattered across many places, and verbose URLs are more likely to be relevant than terse short-hand references.

A future refactor would be to use the bug-reference logic to create the template; but what I have works because I mostly work on Github projects and it’s time to ship it. Also, these CaPFs are available in other contexts, which helps with writing more expressive inline comments.

-1:-- Unfurling of Issue Reference Abbreviations in Github and other Git Forge Issues (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 17, 2023 05:23 PM

Jeremy Friesen: Configuring Emacs to Automatically Prompt Me to Define the Type of Commit

Adding a Function to Help Establish a Habit

my team members began talking prefixing our commit title with the type of commit. The idea being that with consistent prefixing, we can more scan the commit titles to get an overview of what that looks like.

We cribbed our initial list from Udacity Nanodegree Style Guide:

A new feature
A bug fix
Changes to documentation
Formatting, missing semi colons, etc; no code change
Refactoring production code
Adding tests, refactoring test; no production code change
Updating build tasks, package manager configs, etc; no production code change

Our proposal was that at the start of next sprint we’d adopt this pattern for one sprint and then assess. We also had a conversation about the fact that those “labels” consume precious space in the 50 character or so title.

So we adjusted our recommendation to use emojis. We established the following:

feature (A new feature)
bug fix (A bug fix)
docs (Changes to documentation)
style (Formatting, missing semi colons, etc; no code change)
refactor (Refactoring production code)
tests (Adding tests, refactoring test; no production code change)
chore (Updating build tasks, package manager configs, etc; no production code change)

Which means we were only surrendering 2 characters instead of a possible 8 or so.

Given that we were going to be practicing this, I wanted to have Emacs prompt me to use this new approach.

The jf/version-control/valid-commit-title-prefixes defines the glossary of emojis and their meanings:

(defvar jf/version-control/valid-commit-title-prefixes
  '("🎁: feature (A new feature)"
     "🐛: bug fix (A bug fix)"
     "📚: docs (Changes to documentation)"
     "💄: style (Formatting, missing semi colons, etc; no code change)"
     "♻️: refactor (Refactoring production code)"
     "☑️: tests (Adding tests, refactoring test; no production code change)"
     "🧹: chore (Updating build tasks, package manager configs, etc; no production code change)")
  "Team 💜 Violet 💜 's commit message guidelines on <2023-05-12 Fri>.")

I then added jf/git-commit-mode-hook which is added as find-file-hook This hook is fired anytime we find a file and load it into a buffer. .

(cl-defun jf/git-commit-mode-hook (&key (splitter ":") (padding " "))
  "If the first line is empty, prompt for commit type and insert it.

Add PADDING between inserted commit type and start of title.  For
the `completing-read' show the whole message.  But use the
SPLITTER to determine the prefix to include."
  (when (and (eq major-mode 'text-mode)
          (string= (buffer-name) "COMMIT_EDITMSG")
          ;; Is the first line empty?
            (goto-char (point-min))
            (looking-at-p "^$")))
    (let ((commit-type (completing-read "Commit title prefix: "
                         jf/version-control/valid-commit-title-prefixes nil t)))
      (goto-char (point-min))
      (insert (car (s-split splitter commit-type)) padding))))

(add-hook 'find-file-hook 'jf/git-commit-mode-hook)

The jf/git-commit-mode-hook function delivers on the following two scenarios:

Given I am editing a commit message
When I start from an empty message
Then Emacs will prompt me to select the commit type
And will insert an emoji representing that type
Given I am editing a commit message
When I start from a non-empty message
Then Emacs will not prompt me to select the commit type


This function took about 20 minutes to write and helps me create habits around a new process. And if we agree to stop doing it, I’ll remove the hook (maybe keeping the function).

In this practice time, before we commit as a team to doing this, I am already appreciating the improved scanability of the various project’s short-logs. Further this prompt helps remind me to write small commits.

Also, in exploring how to do this function, I continue to think about how my text editor reflects my personal workflows and conventions.

-1:-- Configuring Emacs to Automatically Prompt Me to Define the Type of Commit (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 17, 2023 01:14 PM

Jeremy Friesen: The Why of Linking to a Resource Multiple Times

Peeling Back the Curtain of Some Blogging Wizardy

In Completing Org Links, the author mentioned the following: “I try never to link to something more than once in a single post.”

And I agree!

In a single blog post, I like all of my article’s A-tags to have unique href attributes. See <a>: The Anchor element - HTML: HyperText Markup Language And I also like to use semantic HTML 📖 , such as the CITE-tag See <cite>: The Citation element - HTML: HyperText Markup Language or the ABBR-tag. See <abbr>: The Abbreviation element - HTML: HyperText Markup Language

In my Org-Mode writing, I frequently link to existing Denote documents. Some of those documents do not have a public URL 📖 and others do. During the export from Org-Mode to Hugo, via Ox-Hugo, linked documents that have public URLs will be written up as Hugo shortcodes. And linked documents without public URLs will be rendered as plain text.

The shortcode logic See glossary.html shortcode for implementation details. ensures that each page does not have duplicate A-tags. And in the case of abbreviations, the short code ensures that the first time I render the abbreviation, it renders as: Full Term (Abbreviation) then the next time as Abbreviation; always using the correct ABBR tag and corresponding title attribute.

I also have date links Here I add “date” to the org-link-set-parameters , which export as TIME-tags. See <time>: The (Date) Time element - HTML: HyperText Markup Language And someday, I might get around to writing a function to find the nodes that reference a date’s same year, year/month, and year/month/day.

Another advantage of multiple links in my Org-Mode is that when I shuffle my notes to different files, the backlink utility of Denote and Org-Roam will pick up these new documents

All of this means that my Org-Mode document is littered with links, but on export the resulting to my blog, things become tidier.

So yes, don’t repeat links in blog posts; that’s just a lot of clutter. But for Personal Knowledge Management (PKM 📖) , spamming the links helps me ensure that I’m able to find when and where I mention things.

Which is another reason I have an extensive Glossary of Terms for Take on Rules. All in service of helping me find things.

-1:-- The Why of Linking to a Resource Multiple Times (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 08, 2023 11:37 PM

Jeremy Friesen: Completion at Point Function (CAPF) for Org-Mode Links

Leveraging cape and org-element-map

I write a lot of things using Org-Mode. One function I have wanted is auto-completion of links that already exist in the current Org-Mode buffer. I have created custom links for abbreviations, epigraphs, dates, and glossary of terms.

I spent a bit of time writing that function. I remembered Org-Roam’s completion functions, so I started there for inspiration.

Writing Some Emacs Lisp

I looked to org-roam-complete-link-at-point for inspiration. I need a function that returns the text of the links. Along with the text, I would need the raw-link.

Below is jf/org-links-with-text, the function I wrote. Here’s the link to jf/org-links-with-text.


I updated the jf/org-links-with-text to handle links without labels/text.

(defun jf/org-links-with-text (&optional given-link)
  "Return the `distinct-' `org-mode' links in the

Each element of the list will be a `propertize' string where the
string value is the text of the link and the \"link\" property
will be the :raw-link.

When provided a GIVEN-LINK stop processing when we encounter the
first matching link."

  (let ((links
	   (lambda (link)
	     (when-let* ((left (org-element-property :contents-begin link))
			 (right (org-element-property :contents-end link)))
	       (let ((returning
		       (buffer-substring-no-properties left right)
		       'link (org-element-property :raw-link link))))
		 (if given-link
		     (when (string= given-link returning)
    ;; Ensure that we have a distinct list.
    (if (listp links)
	(-distinct links)
      (list links))))

The above loops through all link elements. Assembling a propertized string with each link it encounters. When provided a given-link it halts processing on the first match. And then returns a list of the matches. I reference Org Element API when writing the function.

Here are some examples of the propertized string section of the code:

  • Given [[https://orgmode.org][Org-Mode]] then return the string Org-Mode with a 'link property of https://orgmode.org.
  • Given [[denote:20230506T202945][Title of Note]] then return the string Title of Note with a 'link property of denote:20230506T202945.

In other words, the CAPF function I’m developing will handle all Org-Mode style links.

With that function, I turned to the inspiration of the org-roam-complete-link-at-point. Below is the function I wrote. Here’s the link to jf/org-capf-links.

;; Cribbed from `org-roam' org-roam-complete-link-at-point
(defun jf/org-capf-links ()
  "Complete links."
  (when (and (thing-at-point 'symbol)
          (not (org-in-src-block-p))
          (not (save-match-data (org-in-regexp org-link-any-re))))
    ;; We want the symbol so that links such performing completion on
    ;; "org-mode" will look for links with the text of org-mode and
    ;; then replace the text "org-mode" with the returned link.
    (let ((bounds (bounds-of-thing-at-point 'symbol)))
      (list (car bounds) (cdr bounds)
        ;; Call without parameters, getting a links (filtered by CAPF
        ;; magic)
        (lambda (text _status)
          ;; We want the properties of that link.  In the case of one
          ;; match, the provided text will have the 'link property.
          ;; However if the
          (let ((link (car (jf/org-links-with-text text))))
            (delete-char (- (length text)))
            (insert "[[" (get-text-property 0 'link link) "]"
                    "[" text "]]")))
        ;; Proceed with the next completion function if the returned
        ;; titles do not match. This allows the default Org capfs or
        ;; custom capfs of lower priority to run.
        :exclusive 'no))))

The above jf/org-capf-links function has three significant parts:

The (when (and… section guards running in a context where things might get confusing.

The (bounds-of-thing-at-point 'symbol) section checks the current item; I could use either 'symbol or 'word; but 'symbol means I can complete or links that have dashes.

The :exit-function, and this is where I spent significant time. In my first round of testing, I had a simple Org-Mode buffer that had one link. When I called the Completion at Point Function (CaPF 📖) function (via TAB) the lambda’s text parameter would have the propertized value.

However, when I had multiple candidates, and selected one, the lambda’s text parameter would not have the propertized value. Hence, I had to go and find again the property.

Last, I wire this into my Org-Mode. To test the functions prior, I had already done this. I use the Corfu and Cape packages. Below is the configuration for my Org-Mode CaPFs :

  (defun jf/org-capf ()
    "The `completion-at-point-functions' I envision using for `org-mode'."
    (setq-local completion-at-point-functions
      (list (cape-super-capf
(add-hook ‘org-mode-hook #’jf/org-capf)


Given that I write between one thousand and four thousand words per day in Org-Mode and I do a lot of linking to code, glossaries, and external sites, I felt it worth the time and energy to write up a CaPF that could help reduce context shifting.

Now, when I write, I can use my TAB completion to provide link candidates to insert.

-1:-- Completion at Point Function (CAPF) for Org-Mode Links (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 07, 2023 03:29 PM

Jeremy Friesen: Dig My Grave: Leveraging the Triple Back-tick in Org Mode

Repurposing Muscle Memory at Your Fingertips

Every time you call my name
I hear the angels say
Dig my grave
John Flansburgh and John Linnel, Dig My Grave

, I was watching one of Gavin Freeborn’s many Emacs videos. I mentally noted his Org-Mode 📖 code block prompting. It was different from what I had.

, I dug a bit into his Emacs configuration; and experimented with org-tempo. But it wasn’t quite what I wanted.

So, I started cleaning up my my Emacs configuration, moving some functions I no longer use into a “graveyard” file.

During this process, I was looking at a function I had commented out. It overloaded the tilde (e.g. ~) key in Org-Mode .

When I typed three consecutive tilde it would would replace those three tilde with a source block, position the cursor inside the block and then call org-edit-special to perhaps create a special editing buffer.

Enter the Overload of Back-tick/Grave

After typing the third consecutive back-tick, the following function:

  • Deletes the three back-ticks.
  • Prompts for the content to insert.
  • Positions into the newly inserted content.
  • Attempts to edit the content in Org-Mode ’s special editing buffer.
(defun dig-my-grave ()
  "Three consecutive graves (e.g. “`”) at the start of the line prompts for
 inserting content.  See `dig-my-grave/templates-alist/org-mode'."
  (if (or (and (> (point) 3)
            (string= (buffer-substring-no-properties
                       (- (point) 3) (point)) "\n``"))
        ;; Account for starting on the first line
        (and (= (point) 3)
          (string= (buffer-substring-no-properties
                     (- (point) 2) (point)) "``")))
    ;; We have just hit our third back-tick at the beginning of the line.
      (delete-char -2)
      ;; I use the alist-get pattern a lot...perhaps a function?
      (let ((value (alist-get (completing-read "Special Content: "
                                  dig-my-grave/templates-alist/org-mode nil t)
                     dig-my-grave/templates-alist/org-mode nil nil #'string=)))
          ;; Let's assume that we're dealing with registered org blocks.
          ((stringp value)
            (insert value) (forward-line -1) (org-edit-special))
          ;; Trust the function
          ((commandp value) (call-interactively value))
          ((functionp value) (funcall value))
          ((ad-lambda-p) (funcall value))
          ;; Time for a pull request
          (t (error "Unprocessable value %s for #'dig-my-grave" value)))))
    (setq last-command-event ?`)
    (call-interactively #'org-self-insert-command)))

For the above dig-my-grave function to work, I need map ` to the function. Near complete control over the environment.

(require 'org)
(define-key org-mode-map (kbd "`") #'dig-my-grave)

And below is the list of templates; They represent the vast majority of the blocks I use in Emacs 📖 :

(defvar dig-my-grave/templates-alist/org-mode
  '(("Bash" . "#+begin_src bash :results scalar replace :exports both :tangle yes\n#+end_src")
    ("Blockquote" . tempel-insert-blockquote_block)
    ("Details and Summary" . "#+begin_details\n#+begin_summary\n\n#+end_summary\n#+end_details")
    ("Emacs Lisp" . "#+begin_src emacs-lisp\n#+end_src")
    ("Org Structure" . org-insert-structure-template)
    ("Plant UML" . "#+begin_src plantuml\n@startuml\n!theme amiga\n\n@enduml\n#+end_src")
    ("Ruby" . "#+begin_src ruby\n#+end_src")
    ("Update" . tempel-insert-update_block))
  "A list of `cons' cells with `car' as the label and `cdr' as
 the value that we'll insert.  Used as the collection for the
 `dig-my-grave' `completing-read'.")

There are three functions:

A Tempel template to insert a blockquote block that I export to my blog post.
A Tempel template to insert an update block that I export to my blog post.
The build-in Org-Mode function to prompt for registered templates.

You can find the above code in the .emacs.d/dig-my-grave.el file of my Emacs configuration.


Now I can repurpose the ingrained Markdown keystrokes for code-blocks into something useful for my Org-Mode writing that I find easy to remember.

Which has me thinking, what are other ways that I might leverage the above function. The progn function hints at generalization. I suppose I’ll hold this and let my mind wander.

In almost all settings other than Markdown, I don’t type ```; how might I bring this to those other modes. Also, I suspect that I want to add the above to a minor-mode, so that I can disable it; in those few cases where I want to type ```.

-1:-- Dig My Grave: Leveraging the Triple Back-tick in Org Mode (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--April 09, 2023 02:32 PM

Jeremy Friesen: Responding to “Things Your Editor Should Have”

Invest in Your Tools

, I listened to Things Your Editor Should Have with Amir Rajan on the Rubber Duck Dev Show.

Amir Rajan outlined four high-level pillars for his software development tools. He needs to be able to…

  • See the function called when I press a key.
  • Run the function without pressing the key.
  • Trivially create my own functions.
  • See the source code for any function.

I have previously written about the Principles of My Text Editor and Revisiting the Principles of My Text Editor. And when I heard Amir’s four points I nodded in deep understanding.

As I grow in experience, understanding, and processes, I want my tools to grow with me. And as my growth is unique to myself, there’s an implication that I should be able to extend the tools to match my frame.

I had spent years with Textmate 📖 , Sublime Text 📖 , and Atom 📖 ; and as Atom reached end of life, I explored Vim 📖 and Visual Studio Code (VS Code 📖) . I wrote two “plugins” for Atom :

And forked a language markdown package to add some Hugo 📖 functionality. The process for writing those packages felt obtuse and cumbersome.

I contrast that to my three years working in Emacs 📖 . Prior to adopting Emacs , I hadn’t programmed in Lisp 📖 and as of , I have defined 231 functions in my Emacs configuration. I ran rg "\((cl-)?defun" | wc in the root directory of my Emacs configuration. These functions help me, in small ways, move fluidly in my day to day writing, reading, coding, and testing.

Looking at the Time Investment

Later in the interview the hosts wonder about the time investment of learning a tool versus the pay-off. I disagree with Amir’s sentiment: “If you’re career trajectory is you want to do 10 years of development and then go into lead and management, then this it’s not worth the investment.”

From where I sit, I can easily say Emacs has been worth my investment; and will continue to pay dividends for me for years to come. Because I intend to continue writing and reading.

In fact, if I were coding less, I think Emacs would become even more valuable. I think about Emacs for Writers; when Jay Dixit, a journalist, presented on Emacs for writing. Watching that, I took notes and sharpened my tools. After watching that recorded meetup, I setup my abbrev_defs to improve my auto-correction.

I write so many things in Org-Mode 📖 and use the multitude of export functions to get the right format for my audience. Using my jf/formatted-copy-org-to-html with surprising regularity, as I write notes in Emacs and then paste them into Slack.

Advice for Junior Developers

This is some great advice:

When you’re a junior developer, you don’t have the experience and intuition to make good decisions about how your code needs to be written…Do you invest in design patterns, or elsewhere? You want to invest in your fundamentals.

The fundamentals you have in your control is typing speed; being able to quickly go to code and read it; those mechanical functions.

You’re in your exploration phase. Don’t commit to VS Code ; do Vim , do Sublime Text …try it all.


I really appreciate Amir’s four pillars enumeration. It resonates with me and draws into focus what I appreciate about Emacs and did not appreciate about VS Code when I last explored using it.

My advice is to broaden your time-frame of consideration for a “text editor”; in my experience I have bounced between developer, manager, and team lead. One constant has been my need to write. As a developer, I’m often writing code. As a manager, I’m more often writing documents. As a lead, I’m writing reviews, technical documents, and tasks breakdowns.

Post Script

, I had breakfast with my dad. He’s one who’s always loved working in the shop; building things. We got to talking about the tools of our trade. And how I use my tools to make more tools.

He nodded in understanding. Knowing what your tools can do, and using them to make more tools, is a virtuous cycle.

-1:-- Responding to “Things Your Editor Should Have” (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--April 04, 2023 12:41 AM

Jeremy Friesen: Expanding on using Tree Sitter in Emacs for Getting Qualified Ruby Method Name

Minor Refinements of a Recently Created Function

I wrote about Using Built-in Emacs 29 Tree Sitter Package to Get Qualified Ruby Function Name. at work I used my jf/treesit/qualified_method_name function about 15 times. That function grabs the method name and it’s class/module scope.

During time, I encountered two edge cases that didn’t work with the implementation I originally wrote. These were self-inflicted edge-cases that related to some idiomatic Ruby 📖 . The first edge case was as follows:

module A::B
  def call

My original code returned #call.

The other edge case was as follows:

module A
  C = Struct.new do
    def call

The original code would return A#call.

I spent a bit of time —five minutes or so—resolving the following test case:

module A::B
  C::D = Struct.new do
    def call

The expected result is A::B::C::D#call. Let’s look at the Abstract Syntax Tree (AST 📖) :

 (module module
  name: (scope_resolution scope: (constant) :: name: (constant))
   (module module name: (constant) ; end)
    left: (scope_resolution scope: (constant) :: name: (constant))
     (call method: (constant) block: . (identifier)
      (do_block do
	(method def body: (identifier) end))
       body: end))))
  body: end))

I use the following two functions:

(cl-defun jf/treesit/qualified_method_name (&key (type "method"))
  "Get the fully qualified name of method at point."
  (if-let ((func (treesit-defun-at-point)))
      ;; Instance method or class method?
      (let* ((method_type (if (string= type
                                       (treesit-node-type func))
                              "#" "."))
             (method_name (treesit-node-text
                           (car (treesit-filter-child
                                 (lambda (node)
                                   (string= "identifier"
             (module_space (s-join "::"
                                    (jf/treesit/module_space func))))
             (qualified_name (concat module_space method_type
        (message qualified_name)
        (kill-new (substring-no-properties qualified_name)))
    (user-error "No %s at point." type)))
(defun jf/treesit/module_space (node)
  (when-let* ((parent (treesit-parent-until
                       (lambda (n) (member (treesit-node-type n)
              (parent_name (treesit-node-text
                              (lambda (n)
                                (member (treesit-node-type n)
    (list (jf/treesit/module_space parent) parent_name)))

The key was adding assignment to the list of parents and scope_resolution to the list of parent’s child nodes to check.

You can see my updated code here.

-1:-- Expanding on using Tree Sitter in Emacs for Getting Qualified Ruby Method Name (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 28, 2023 01:47 AM

Jeremy Friesen: Using Built-in Emacs 29 Treemacs Package to Get Qualified Ruby Function Name

A Rainy Day Coding Practice Session

When I’m writing about or in Ruby 📖 code, sometimes I want to grab the qualified method name. For example, let’s say I have the following Ruby code:

module Hello
  module World
    def foo

    def self.call

The qualified method name for the method #foo would be Hello::World#foo. The qualified method name for the singleton method .call is Hello::World.call. A Ruby documentation convention is that instance methods are prefix with a # and singleton methods are prefixed with a . or ::.

Using treesit-explore-mode, I was able to quickly refine my recursive queries. Below is treesit’s rendering of the Abstract Syntax Tree (AST 📖) of the above Ruby code:

 (module module name: (constant)
   (module module name: (constant)
     (method def body: (identifier)
      (body_statement (simple_symbol))
     (singleton_method def object: (self) . body: (identifier)
      (body_statement (simple_symbol))
    body: end))
  body: end))

, in a moment of dreary skies and sleeping dogs, I hacked together the following functions:

Copy the qualified method name to the paste buffer (e.g. the kill-ring).
Recurse up from a node to create a list of the module/class ancestors.
(require 'treesit)
(cl-defun jf/treesit/qualified_method_name (&key (type "method"))
  "Get the fully qualified name of method at point."
  (if-let ((func (treesit-defun-at-point)))
      ;; Instance method or class method?
      (let* ((method_type (if (string= type
                                       (treesit-node-type func))
                              "#" "."))
             (method_name (treesit-node-text
                           (car (treesit-filter-child
                                 (lambda (node)
                                    (treesit-node-type node)))))))
             (module_space (s-join "::"
                                    (jf/treesit/module_space func))))
             (qualified_name (concat module_space
        (message qualified_name)
        (kill-new (substring-no-properties qualified_name)))
    (user-error "No %s at point." type)))

;; An ugly bit of code to recurse upwards from the node to the "oldest"
;; parent.  And collect all module/class nodes along the way. This will
;; return a series of nested lists.  It's on the originating caller to
;; flatten that list.
(defun jf/treesit/module_space (node)
  (when-let* ((parent (treesit-parent-until
                       (lambda (n) (member (treesit-node-type n)
                                           '("class" "module")))))
              (parent_name (treesit-node-text
                            (car (treesit-filter-child
                                  parent (lambda (n)
                                            (treesit-node-type n))))))))
    (list (jf/treesit/module_space parent) parent_name)))

This is most certainly a rainy day kind of project; one that helped me learn just a bit more about the treesit package.


The list returned by jf/treesit/module_space is '(nil ("Hello" ("World"))); which is a ugly but workable. Perhaps someone will write to me with a refactor of this code.

-1:-- Using Built-in Emacs 29 Treemacs Package to Get Qualified Ruby Function Name (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 25, 2023 10:08 PM

Jeremy Friesen: Using Built-in Emacs 29 Tree Sitter Package to Get Qualified Ruby Function Name

A Rainy Day Coding Practice Session

When I’m writing about or in Ruby 📖 code, sometimes I want to grab the qualified method name. For example, let’s say I have the following Ruby code:

module Hello
  module World
    def foo

    def self.call

The qualified method name for the method #foo would be Hello::World#foo. The qualified method name for the singleton method .call is Hello::World.call. A Ruby documentation convention is that instance methods are prefix with a # and singleton methods are prefixed with a . or ::.

Using treesit-explore-mode, I was able to quickly refine my recursive queries. Below is treesit’s rendering of the Abstract Syntax Tree (AST 📖) of the above Ruby code:

 (module module name: (constant)
   (module module name: (constant)
     (method def body: (identifier)
      (body_statement (simple_symbol))
     (singleton_method def object: (self) . body: (identifier)
      (body_statement (simple_symbol))
    body: end))
  body: end))

, in a moment of dreary skies and sleeping dogs, I hacked together the following functions:

Copy the qualified method name to the paste buffer (e.g. the kill-ring).
Recurse up from a node to create a list of the module/class ancestors.
(require 'treesit)
(cl-defun jf/treesit/qualified_method_name (&key (type "method"))
  "Get the fully qualified name of method at point."
  (if-let ((func (treesit-defun-at-point)))
      ;; Instance method or class method?
      (let* ((method_type (if (string= type
                                       (treesit-node-type func))
                              "#" "."))
             (method_name (treesit-node-text
                           (car (treesit-filter-child
                                 (lambda (node)
                                    (treesit-node-type node)))))))
             (module_space (s-join "::"
                                    (jf/treesit/module_space func))))
             (qualified_name (concat module_space
        (message qualified_name)
        (kill-new (substring-no-properties qualified_name)))
    (user-error "No %s at point." type)))

;; An ugly bit of code to recurse upwards from the node to the "oldest"
;; parent.  And collect all module/class nodes along the way. This will
;; return a series of nested lists.  It's on the originating caller to
;; flatten that list.
(defun jf/treesit/module_space (node)
  (when-let* ((parent (treesit-parent-until
                       (lambda (n) (member (treesit-node-type n)
                                           '("class" "module")))))
              (parent_name (treesit-node-text
                            (car (treesit-filter-child
                                  parent (lambda (n)
                                            (treesit-node-type n))))))))
    (list (jf/treesit/module_space parent) parent_name)))

This is most certainly a rainy day kind of project; one that helped me learn just a bit more about the treesit package.


The list returned by jf/treesit/module_space is '(nil ("Hello" ("World"))); which is a ugly but workable. Perhaps someone will write to me with a refactor of this code.

-1:-- Using Built-in Emacs 29 Tree Sitter Package to Get Qualified Ruby Function Name (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 25, 2023 10:08 PM

Jeremy Friesen: Creating an Emacs Function to Create Yardoc Stubs

When One Pathway Fails, Try Another

This afternoon, I was exploring some Tree Sitter 📖 functions in Emacs 📖 . I wanted to take a Ruby 📖 ’s method’s signature and create Yardoc stubs from the method parameters.


Let’s say I have the following Ruby method definition:

def call(foo, bar:, baz: :default, **kwargs)
  # do stuff

I wanted to call a function and update the buffer as follows:

# @param foo [Object]
# @param bar [Object]
# @param baz [Object]
# @param kwargs [Object]
def call(foo, bar:, baz: :default, **kwargs)
  # do stuff


I received an email pointing out that I had mixed the treesit (e.g. treesit-inspect-node-at-point) and tree-sitter (e.g. tsc-get-child-by-field) functions.

Gah! I had that in my kill-ring. I also tried the following to no avail:

(let ((func-node (tree-sitter-node-at-point 'method))
        (params (tsc-get-child-by-field func-node ':method_parameters)))
    (message "%s" params))

The email also pointed out that my “Reply by Email” link was broken; so I fixed that.

Thank you dear reader!

The Interlude and Solution

I was encountering problems with tree-sitter functionality. The tree-sitter package is an external package. The treesit is a built-in package in Emacs 29. I prefer tree-sitter as it’s more performant in my use case. The following emacs-lisp writes a nil message:

(let ((func-node (treesit-inspect-node-at-point 'method))
      (params (tsc-get-child-by-field func-node ':method_parameters)))
  (message "%s" params))

The above, in my reading, should’ve found the node that had the method parameters.

Running into those problems, I took a different path. String parsing and regular expressions. Below is that solution:

(defun jf/ruby-mode/yardoc-ify ()
  "Add parameter yarddoc stubs for the current method."
  ;; Remember where we started.
    ;; Goto the beginning of the function
    ;; Move to just after the first (
    (search-forward "(")
    ;; Move back to just before the (
    ;; Select parameters declaration
    ;; Copy that
    (copy-region-as-kill (point) (mark))
    ;; Split apart the parameters into their identifiers
    (let ((identifiers (mapcar (lambda (token)
                             "[^a-z|_]" ""
                             (car (s-split " "
                                           (s-trim token)))))
                          (s-split "," (substring-no-properties
                                        (car kill-ring))))))
      ;; Go to the beginning of the function again
      ;; Now insert the identifiers as yardoc
      (insert "##\n"
              (s-join "\n" (mapcar
                            (lambda (param)
                              (concat "# @param "
                                      " [Object]"))
-1:-- Creating an Emacs Function to Create Yardoc Stubs (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 19, 2023 02:42 AM

Jeremy Friesen: Spending a Bit of Time Reviewing Consult Emacs Package

Exploring Functionality by Re-Reading Documentation

When I first adopted the Consult package, I scanned the documentation and setup a basic configuration. I then went about using the basics and hacking functionality. I spent some time reviewing Consult’s documentation.

Below is the original advice-add for my Consult configuration. The functionality was as follows: when I have text highlighted, use the highlighted text for my line search (e.g. consult-line) and file search function (e.g. consult-ripgrep).

(defun jf/consult-first-param-is-initial-text (consult-fn &rest rest)
  "Advising function around CONSULT-FN.

The CONSULT-FN's first parameter should be the initial text.

When there's an active region, use that as the first parameter
for CONSULT-FN.  Otherwise, use an empty string the first
parameter.  This function handles the REST of the parameters."
  (apply consult-fn
         (when (use-region-p)
            (region-beginning) (region-end)))

(defun jf/consult-ripgrep-wrapper (consult-fn &optional dir given-initial)
  "Advising function around CONSULT-FN.

DIR and GIVEN-INITIAL match the method signature of `consult-wrapper'."
  (interactive "P")
  (let ((initial (list (or given-initial
                           (when (use-region-p)
                             (buffer-substring (region-beginning)
    (apply consult-fn dir initial)))
(advice-add #'consult-line
            :around #'jf/consult-first-param-is-initial-text
            '((name . "wrapper")))
(advice-add #'consult-ripgrep
            :around #'jf/consult-ripgrep-wrapper
            '((name . "wrapper"))))

After reading the Consult README, I removed my customization and added the following:

(consult-customize consult-line
                   :initial (when (use-region-p)
                               (region-beginning) (region-end))))

The above consult-customize duplicates many lines of code I had previously written.

There are three distinct benefits:

  1. I’m following the documented pattern of customization.
  2. I’m not over-riding a method and relying on stable method signatures.
  3. I’m writing less code.

Put another way, Daniel Mendler has implemented Consult to be extensible.


I also took this time to review other functionality; learning about consult history functionality. When I run consult-ripgrep, I can invoke consult-history (which I’ve bound to C-c h). This then shows past searches and lets me pick from them.

I’ve been spending time actively thinking about how I’m using my editor:

  • Shifting key bindings to ease how my fingers stretch and move.
  • Reading package customization options.
  • Practicing new to me navigation shortcuts (looking at you Avy 📖 ).

Why? Because more than ever my writing is my thinking. And improving my reflexes on moving throughout my editor helps reduce mental friction; which for me reduces fatigue and increases the chances of retention during long-running tasks.

-1:-- Spending a Bit of Time Reviewing Consult Emacs Package (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 15, 2023 01:52 AM

Jeremy Friesen: Note Taking Apps I'd Consider (If I Didn't Use Emacs)

A Quick Intro to Other Open Source Note Taking Tools

Over the I’ve talked with a few folks looking at improving their note-taking game. As part of those conversations, I try to find out their use-case.

Do they want to get better at writing? Are they looking to synchronize notes across their machine? Are they looking at integration with their code and text editor?

For myself, I want my note taking functionality incorporated in my text editor. That way I can further strengthen existing muscle memory short-cuts and perhaps open pathways to consider more.

Were I not using Emacs 📖 and Org-Mode 📖 (with Denote 📖 ) I’d consider the following:

NB 📖
a command line and local web note‑taking, book-marking, archiving, and knowledge base application.
Logseq 📖
a privacy-first, open-source knowledge base that works on top of local plain-text Markdown and Org-mode files.
Dendron 📖
an open-source, local-first, markdown-based, note-taking tool.
VimWiki 📖
A personal wiki for Vim—interlinked, plain text files written in a markup language.

Were I using Visual Studio Code 📖 , I’d go with Dendron . If I used Vim 📖 , I’d consider VimWiki or NB . And if I didn’t write code? I’d consider Logseq .

All of the above options are open-source, something I consider critical to my note-taking. I do not want vendor lock-in for the information I’m gathering, organizing, and referencing.

Further, each of those applications write notes to your file system in a recoverable manner. If the application goes away, you still have the raw files written in a consistent and mostly portable format.

-1:-- Note Taking Apps I'd Consider (If I Didn't Use Emacs) (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 13, 2023 01:51 PM

Jeremy Friesen: Take on Rules Blog Writing and Publishing Playbook

Documenting my High Level Workflow for Bringing the Bits to the Web

As of I build Take on Rules via a Static Site Generator (SSG 📖) . Hugo 📖 to be precise. I write locally, run a build process which generates static HTML 📖 pages. I push those pages to a host which is then serves those via a bare-bones web-server. I cannot express how much I appreciate that my personal blog eschews a complex web application server. My web server has no “keys to the kingdom.” It simply knows about the static pages that I give it to render.

Throughout this post, I reference functions that are in my my Emacs configuration.

Preliminary Work

Throughout my days, I’m reading code, writing code, documentation, and commit messages; I’m thinking through different implementation approaches for processes; reading blog posts around my hobbies and my profession; reading books for learning and pleasure. All of which I might be writing down little pointers or notes to what I am doing.

Those little pointers begin to take shape in my mind.

Composing a Blog Post

In Emacs 📖 . I start writing my blog post by calling M-x jf/denote-create--blog-posts--default. I might instead move a note into my blog-post sub-directory; which means it’s something I might make public. This prompts me to:

While writing I leverage several different commands, some of those are as follows:

Search for and insert a link to an existing document in my Personal Knowledge Management (PKM 📖) .
Pick the type of link, describe it then insert a link to the corresponding resource. Links can be dates, abbreviations, epigraphs, files, URLs 📖 , and many other things provided by the default Org-Mode .
To insert src, details, summary, marginnote, inline_comments
To create a footnote, which converts into a sidenote when exporting to my blog. See Hacking Org-Mode Export for Footnotes as Sidenotes.
Some custom completion at point functions; in particular auto-completing existing abbreviations; beware when I’m in a list the capf does not work.

Another important function is my jf/org-mode/capture/insert-content-dwim. This builds on the Org-Mode clocking function. See Clocking Work Time (The Org Manual) for more documentation on Org-Mode “clocking.” When I start a clock on a blog post, I can use jf/org-mode/capture/insert-content-dwim to quickly capture text and context from other buffers.

Leveraging HTML Semantics via Macro Substitution

Org-Mode is a great markup format, but it doesn’t map to many semantic elements of HTML . Something that I appreciate. And because I’ve chosen semantic markup, I can more readily redesign my site using CSS 📖 .

I also have several Org-Mode replacement macros. See Macro Replacement (The Org Manual).

This will export the text into a CITE element.
This will export the text into a I element; once meaning italics and now meaning idiomatic.
This will export the text into a KBD element.

Each of those macros have corresponding Tempel templates which also provide completing-read functionality by looking up all references on my local file-system. See my templates file.

I have a bq template that I can use to generate a BLOCKQUOTE element with cite, url, and attribution attributes. See the bq template definition here.

, I have just over 3000 entries in my PKM . 858 of those entries are blog posts I’ve published. In my Denote Emacs Configuration blog post, I wrote about the different domains of my PKM :

the blog posts I write
the epigraphs and quotes I’ve collected
my link farm of terms and concepts
top-level indices to organize concepts
a general junk drawer
people I know or don’t know
my work

By linking my “to be published” blog posts to internal notes, I can look at my internal notes and see their backlinks. That is what blog posts link to what notes. This is done through the denote-link-backlinks function. See Denote’s documentation on the Backlinks buffer.

Exporting a Blog Post

I use the Ox-Hugo 📖 package for exporting my blog post. I have written several overrides to the export process:

  • Configure to export back-ticks (e.g. `) for code blocks, instead of indentations.
  • Override foot note generation for side notes.
  • Override how I export links, to leverage my glossary and other custom mechanisms.

One advantage in overriding how my blog exports foot notes is that if when I choose to export my blog post into other formats (e.g. a PDF 📖 via LaTeX 📖 ), I still get foot notes.

Reviewing the Blog Post

With my blog post exported, I switch over to ~/git/takeonrules.source/, a Hugo project that builds the static site that is Take on Rules.

I run the following Hugo command: hugo serve -D. The -D flag tells Hugo to build and serve both published and draft pages. This starts a web server which can be accessed at https://localhost:1313. The default port when locally running Hugo . (the default port for Hugo ).

Publishing a Blog Post

Having checked the blog post on my local machine, I run the following command: bundle exec rake one_shot_publish.

That command does many things: first, it runs audits to ensure well-formed content. Somewhere around version 0.78.0 of Hugo , I was encountering rendering snafus. If memory serves, it was around the time that the maintainers were switching from one Markdown processor to another. It also fetches data from my PKM and generates metadata for my site. The List of All Tables being one of those. Eventually it creates the public directory and pushes that to my Virtual Private Server (VPS 📖) .

Socializing the Blog Post

Sometimes I’ll syndicate to other platforms (e.g. the DEV Community 📖 ). Other times I’ll post to the corresponding sub-reddit. I might also post a quick link up on Mastodon 📖 . Once upon a time, I would also post to Twitter. But I Deactivated my Twitter Account.


I wrote this playbook as a meditation on what steps I take to bring things forward from my personal note taking process into blog posts. These days, the notes I take are now readily available to convert into blog posts for sharing.

-1:-- Take on Rules Blog Writing and Publishing Playbook (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 12, 2023 02:24 PM

Jeremy Friesen: Coloring Regular Expression via Modus Themes for Treesit Faces

Adding a Bit More Color to My Baseline Theme

In this post, I want to show how I added a bit of additional color to my theme for faces assigned by the built-in Emacs 📖 29 treesit package.


adopting Emacs , I have used the Modus Themes. I appreciate Protesilaos Stavrou (Prot 📖) ’s focus on accessibility for the Modus Themes as well as a commitment to accessible documentation.

When you dive deeper into the Modus Themes, you see how customizable and extensible the themes are. I suspect it could be used as a theme-building framework.

In Adding Consistent Color to Emacs Mode Line and iTerm Tab for Various Projects, I showed one way that I’m leveraging Modus Themes to set my mode-line and iTerm2 tab colors by project.

My Emacs Configuration

I’m favoring treesit modes over other modes as Tree Sitter 📖 provides a more robust syntax highlighting.

Below is my treesit and treesit-auto configuration. The treesit package is part of Emacs 29. The treesit-auto package provides automatic translations from non-treesit modes to their corresponding treesit modes. A very helpful package to help bridge other packages that assume one language mode (e.g. ruby-mode) when treesit sets it to a different mode (e.g. ruby-ts-mode).

(use-package treesit
  :custom (treesit-font-lock-level 4)
  :straight (:type  built-in))

(use-package treesit-auto
  :straight (:host github :repo "renzmann/treesit-auto")
  :config (setq treesit-auto-install 'prompt)

Next is the code I use to set my modus-theme. I prefer the “tinted” variation as the day mode (e.g. “operandi”) it’s a bit gentler on my eyes. Most importantly is using modus-themes-load-theme, as that is the function that calls the modus-themes-after-load-theme-hook. More on that in a bit.

(defun jf/emacs-theme-by-osx-appearance ()
  "Set theme based on OSX appearance state."
  (if (equal "Dark" (substring
                      "defaults read -g AppleInterfaceStyle")
                     0 4))
      (modus-themes-load-theme 'modus-vivendi-tinted)
    (modus-themes-load-theme 'modus-operandi-tinted)))


I used the describe-text-properties function to inspect the opening regular expression character. It had the font-lock-regexp-face face.

Likewise, I did the following for Ruby’s string interpolation function (e.g. #{}). That had the face of font-lock-misc-punctuation-face.

Following the Modus Themes documentation, I added the code for setting the foreground color.

(defun jf/modus-themes-custom-faces ()
       ((,c :foreground ,green-warmer)))
       ((,c :foreground ,red-cooler))))))

(add-hook 'modus-themes-after-load-theme-hook

Show me the Screenshots

The modus-vivendi-tinted theme with colorized regular expression and string interpolation.

The modus-operandi-tinted theme with colorized regular expression and string interpolation.

-1:-- Coloring Regular Expression via Modus Themes for Treesit Faces (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 08, 2023 01:47 PM

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!