Jeremy Friesen: An Emacs Function for Wrapping Ruby Functions

One of my “quality of life” improvements in my text editor is related to Rubocop 📖 ; a style guide enforcer that raises issues about complexity, formatting, and other things.

When I’m coding and feel I need to ignore Rubocop ’s opinion, I will tell Rubocop to ignore the violation. I do this by adding a “disable” comment before the violation and an “enable” comment after the violation.

I wrote an Emacs 📖 function that leverages Tree Sitter 📖 to insert the comments. Note: The jf/rubocop/list-all-cops is a constant I’ve defined that lists the numerous checks

update:

I refactored the original jf/treesit/wrap-rubocop to no longer kill the region and then yank the region back in. I also extracted the method that determines what is the current context.

The extraction added a new feature; namely attempt to determine the containing class/module declaration for point.

Read the latest code here.

  (defun jf/treesit/wrap-rubocop (&optional given-cops)
    "Wrap the current ruby region by disabling/enabling the GIVEN-COPS."
    (interactive)
    (if (derived-mode-p 'ruby-ts-mode 'ruby-mode)
      (if-let ((region (jf/treesit/derive-region-for-rubocop)))
        (let ((cops
                (or given-cops
                  (completing-read-multiple "Cops to Disable: "
                    jf/rubocop/list-all-cops nil t))))
          (save-excursion
            (goto-char (cdr region))
            (call-interactively #'crux-move-beginning-of-line)
            (let ((indentation (s-repeat (current-column) " ")))
              (goto-char (cdr region))
              (insert "\n"
                (s-join "\n"
                  (mapcar
                    (lambda (cop)
                      (concat indentation "# rubocop:enable " cop))
                    cops)))
              (goto-char (car region))
              (beginning-of-line)
              (insert
                (s-join "\n"
                  (mapcar
                    (lambda (cop)
                      (concat indentation "# rubocop:disable " cop))
                    cops))
                "\n"))))
        (user-error "Not a region nor a function"))
      (user-error "%s is not derived from a ruby mode" major-mode)))

  (defun jf/treesit/derive-region-for-rubocop ()
    "Return `cons' of begin and end positions of region."
    (cond
      ;; When given, first honor the explicit region
      ((use-region-p)
        (cons (region-beginning) (region-end)))
      ;; Then honor the current function
      ((treesit-defun-at-point)
        (cons (treesit-node-start (treesit-defun-at-point))
          (treesit-node-end (treesit-defun-at-point))))
      ;; Then fallback to attempting to find the containing
      ;; class/module.
      (t
        (when-let ((node
                     (treesit-parent-until
                       (treesit-node-at (point))
                       (lambda (n) (member (treesit-node-type n)
                                     '("class" "module"))))))
          (cons (treesit-node-start node) (treesit-node-end node))))))

I’ve bound this to C-c w r; mnemonically this is “Command Wrap Rubocop.”

A few times per week, I use the jf/treesit/wrap-rubocop function. A small improvement in my work experience.

-1:-- An Emacs Function for Wrapping Ruby Functions (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 17, 2024 11:06 PM

Jeremy Friesen: Completion at Point for Org Macros

I’ve created a few different macros within Org-Mode 📖 . What I wanted was to enable Completion at Point Function (CaPF 📖) for macros defined within the current buffer. Sidenote See Macro Replacement (The Org Manual) for documentation on Org-Mode macros.

I had previously implemented Completion at Point Function (CAPF) for Org-Mode Links and used that for guidance.

I broke this down into three conceptual functions:

First, create a list of all matches for a regular expression within the buffer. Implemented via jf/regexp-matches-for-text.

(defun jf/regexp-matches-for-text (regexp &optional string)
  "Get a list of all REGEXP matches in the STRING."
  (save-match-data
    (seq-uniq
     (let ((string (or string (buffer-string)))
           (pos 0) matches)
       (while (string-match regexp string pos)
         (let ((text (match-string 0 string)))
           (set-text-properties 0 (length text) nil text)
           (push text matches))
         (setq pos (match-end 0)))
       matches))))

Second, get all of the existng macros within the current buffer. This leverages jf/regexp-matches-for-text and is implemented via jf/org-macros.

(defun jf/org-macros (&optional given-macro)
  (let ((macros (jf/regexp-matches-for-text "{{{[^}]+}}}")))
    (if given-macro
      (when (member given-macro macros) (list given-macro))
      macros)))

The nuance of providing a given-macro relates to the third function. Namely a CaPF conformant defintion. This is implemented in jf/org-capf-macros.

(defun jf/org-capf-macros ()
  "Complete macros.

And 3 shall be the magic number."
  ;; We're looking backwards for one to three '{' characters.
  (when (and (looking-back "{+" 3)
          (not (org-in-src-block-p)))
    (list
      ;; The beginning of the match is between 1 and 3 characters before
      ;; `point'.  The math works out:
      ;; `point' - 3 + number of '{' we found
      (+ (- (point) 3)
        (string-match "{+" (buffer-substring (- (point) 3) (point))))
      (point)
      ;; Call without parameters, getting all of the macros.
      (jf/org-macros)
      :exit-function
      (lambda (text _status)
        (let ((macro (car (jf/org-macros text))))
          (delete-char (- (length text)))
          (insert macro)))
      ;; 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)))

When I add jf/org-capf-macros to my Org-Mode CaPF and I type { then Tab, I am presented with a list of existing macros I’ve declared within the buffer. And I can select those and insert the macro definition at point.

I’ve added the above code to my Emacs configuration.

Conclusion

Given that I use Org-Mode macros for transforming text to specific inline markup, I found that when I was writing something that used a macro, I would often repeat usage of that macro.

For those curious, here are some of my macros. And here are some templates for populating those macros.

My templates for inserting a macro scour my Personal Knowledge Management (PKM 📖) for macro declarations and provide a list to select from. This is a bit slower and less specific than searching within the buffer.

-1:-- Completion at Point for Org Macros (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 16, 2024 10:18 PM

Jeremy Friesen: An Evening Reading Documentation Leads to Discovery

I was exploring the Info section of Emacs documentation. I started reading about the Ripgrep Emacs package.

I learned that in the search results buffer of the rg command, I could save and name the query for later reference. I could then list out my saved queries and execute them.

I also learned about the Transient 📖 menu option. By default bound to C-c s, this command brings up menu where I can specify the Ripgrep 📖 command switches, different search strategies, list the saved queries, along with other things.

Digging into the Info, I found the following:

-- ELisp Function: (rg-define-search name, &rest, args)

    This macro can be used to define custom search functions in a
    declarative style.  Default implementations for common behavior is
    available and custom forms can also be used.

Using the example, I wrote up the following, which will conditionally add the “Projects” section to the search menu, binding the key d to the search labeled “Dotemacs”. That search will look within ~/git/dotemacs/, searching all files. Sidenote You can see my rg package configuration up on GitHub.

(when (f-dir-p "~/git/dotemacs/")
    (rg-define-search rg-projects-dotemacs
      "Search Dotemacs."
      :dir "~/git/dotemacs/"
      :files "*.*"
      :menu ("Projects" "d" "Dotemacs")))

Given the nature of my work (and hobbies), I’m often working within one project and want to search another project. It is a useful discovery to now have the declarative structure for persisting and exposing those common queries.

Screenshot of Menu

The “Projects” section is one added by calling rg-define-search. The other sections are part of the baseline menu configuration.

An Emacs textual buffer with sections for Switches, Options, Search, Manage, and Projects.
-1:-- An Evening Reading Documentation Leads to Discovery (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 02, 2024 01:24 PM

Jeremy Friesen: Quality of Life Improvement for Entering and Exiting Magit

I use Magit 📖 everyday for most of my typical Git 📖 operations (e.g. writing commit messages, interactive rebasing, reviewing logs, creating pull requests, etc.)

What I have wanted is that when I call magit-status that it would “close” everything and be the sole focus. In Emacs 📖 that would mean the Magit buffer would be the only window in the frame. And when I was done in Magit , I would sure love to restore the view to how it was before.

I had found some hacks that used advising functions. I instead did some digging by introspecting the code and found the following two functions:

And I found two custom variables:

magit-display-buffer-function
“The function used to display a Magit buffer.”
magit-bury-buffer-function
“The function used to bury or kill the current Magit buffer.”

Each of these variables had several options. And after some further reading of the docstrings, I settled on the following:

(setq magit-display-buffer-function
      #'magit-display-buffer-fullframe-status-v1)
(setq magit-bury-buffer-function
      #'magit-restore-window-configuration)

See the documentation for magit-display-buffer-function and documentation for magit-bury-buffer-function.

-1:-- Quality of Life Improvement for Entering and Exiting Magit (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 02, 2024 12:27 AM

Jeremy Friesen: Configuring Consult Imenu Narrowing for Ruby

I use the Consult package. During Protesilaos Stavrou 📖 ’s Emacs: basics of search and replace video, he demonstrated the consult-imenu function for Emacs Lisp 📖 . In particular the semantic narrowing functionality.

The baseline Consult package declares consult-imenu-config Sidenote See Github’s consult-imenu-config source code. as follows:

(defcustom consult-imenu-config
  '((emacs-lisp-mode :toplevel "Functions"
                     :types ((?f "Functions" font-lock-function-name-face)
                             (?m "Macros"    font-lock-function-name-face)
                             (?p "Packages"  font-lock-constant-face)
                             (?t "Types"     font-lock-type-face)
                             (?v "Variables" font-lock-variable-name-face))))
  "Imenu configuration, faces and narrowing keys used by `consult-imenu'.

For each type a narrowing key and a name must be specified.  The
face is optional.  The imenu representation provided by the
backend usually puts functions directly at the toplevel.
`consult-imenu' moves them instead under the type specified by
:toplevel."
  :type '(repeat (cons symbol plist))
  :group 'consult)

With that baseline configuration, when I call consult-imenu in an Emacs Lisp file, I see the symbols groups by the above types (e.g. Functions, Macros, Packages, Types, and Variables).

Configuring for Ruby

Here’s my configuration for Ruby mode and the sibling Ruby Tree Sitter 📖 mode.

(require 'consult-imenu)
(dolist (ruby '(ruby-mode ruby-ts-mode))
  (add-to-list 'consult-imenu-config
    `(,ruby
       :toplevel "Method"
       :types ((?p "Property" font-lock-variable-name-face)
                (?c "Class" font-lock-type-face)
                (?C "Constant" font-lock-type-face)
                (?M "Module" font-lock-type-face)
                (?m "Method" font-lock-function-name-face)))))

Without the configuration consult-imenu will show the above symbols but will not categorize them into groups that I can further narrow.

Let’s look at the following Ruby code.

class BasicProgram
  DEFAULT_HELLO = "hello"
  DEFAULT_WORLD = "world"

  def initialize(hello: DEFAULT_HELLO, world: DEFAULT_WORLD)
    @hello = hello
    @world = world
  end

  attr_reader :hello, :world

  def to_s
    "#{hello} #{world}"
  end
end

puts BasicProgram.new.to_s

In the above code, consult-imenu leverages the outline functionality to categorize the information as follows, and then render in the mini-buffer:

  • Class
    • BasicProgram
  • Constant
    • BasicProgram DEFAULT_HELLO
    • BasicProgram DEFAULT_WORLD
  • Property
    • BasicProgram hello
    • BasicProgram world
  • Method
    • BasicProgram new
    • BasicProgram to_s

Within the minibuffer, I can then search as normal or narrow to a single category by typing the category key and space.

For example I could narrow to “Methods” by typing m then space. That narrows the above list to the following:

  • BasicProgram new
  • BasicProgram to_s

I could then type to further narrow the text.

Conclusion

I find that I navigate code in many different ways, and when I find a means that improves organizational overview as well as navigation, I take notice.

-1:-- Configuring Consult Imenu Narrowing for Ruby (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--February 28, 2024 02:17 PM

Jeremy Friesen: Emacs Function to Assign Org-Mode Property to Matching Criteria

, I wrote my Update on the Campaign Status Document. I had manually set the alignment of several Non-Player Characters (NPCs 📖) ; but I thought “Maybe I should instead clear those out and randomize?”

And I started thinking about how I might automatically update (or add) new properties.

What I wanted was to select a top-level headline, select a property name, and pick a random table. Sidenote As per my random-tables Emacs package 📖 . With those chosen, I then populate all immediate sub-headings with the property and a randomized roll on the table. Sidenote A “table” can be a dice expression.

Below is that implementation.

(defun jf/campaign/populate-property-draw-value (&optional headline property table)
  "Populate HEADLINE's direct children's empty PROPERTY with roll on given TABLE.

When given no HEADLINE, prompt for ones from the document.

When given no PROPERTY, prompt for a property.

When given no TABLE, prompt for an expression to evaluate via
`random-table/roll'."
  (interactive)
  (let* ((level 1)
          (headline (or headline
                      (completing-read "Headline: "
                        (jf/org-get-headlines level) nil t)))
          (property (or property
                      (org-read-property-name)))
          (table (or table
                   (completing-read "Table: "
                     random-table/storage/tables)))
          (random-table/reporter
            ;; Bind a function that will only output the results of the
            ;; table, excluding the expression that generated the
            ;; results.
            (lambda (expression results) (format "%s" results))))
    (unless (org--valid-property-p property)
      (user-error "Invalid property name: \"%s\"" property))
    (save-excursion
      ;; Now that we have the information, let’s start rolling on some
      ;; tables.
      (dolist
        ;; Because we will not be recalculating heading positions, we
        ;; need to reverse the list, as updates to the last element will
        ;; not alter the position of the next to last element.
        (subheading (reverse
                      (org-element-map
                        (org-element-parse-buffer)
                        'headline
                        (lambda (el)
                          (and
                            (= ;; We want the children of the given
			       ;; level
                             (+ 1 level)
			     (org-element-property :level el))
                            (string=
                              ;; Match the text
                              (org-element-property
                                :raw-value
                                (car (org-element-lineage el)))
                              headline)
                            ;; Don't clobber already set values
                            (not (org-element-property property el))
                            ;; We found a match now return it
                            el)))))
        ;; We have finally found the element, now roll on the table and
        ;; populate.
        (org-entry-put
          (goto-char (org-element-property :begin subheading))
          property (random-table/roll table))))))

Conclusion

On the surface, Org-Mode 📖 syntax appears to be a variant of Markdown. But the true beauty shines when you consider the additional structure and functionality. The Org Element API provides useful guidance. But even more valuable is being able to see the source code.

I was able to look at the org-set-property function to find a few bits of useful logic that would help me avoid repetition. And from there I have a reasonably robust function.

One refinement obvious refinement is that the table parameter could instead be a function, which is then called for each element.

update

The previous code worked for a small test case, but once I test against a more complicated situation, it failed.

I intuited there would be a problem, as the previous comments indicated; but in my haste I didn’t check and verify.

I have updated the code and references to reflect those changes.

-1:-- Emacs Function to Assign Org-Mode Property to Matching Criteria (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--February 24, 2024 10:15 PM

Jeremy Friesen: Quick and Dirty Function to Sort My Feed

I use the elfeed 📖 for reading RSS 📖 feeds. And for managing the feeds, I use elfeed-org 📖 package to organize and manage those feeds.

Recently, I started publishing my blogroll this is done by exporting an Outline Processor Markup Language (OPML 📖) document from elfeed .

I was a little bothered that I didn’t export those items in what I consider a well-sorted order.

So, I set about exploring how to fix that. I’ve been using the org-sort function for other organizational tasks. Sidenote See Structure Editing (The Org Manual) for documentation on org-sort.

When calling org-sort, I can provide a sorting function.

And this is what I have hacked together.

First, here’s a utility function to pull out the domain and top-level domain. The function will convert https://www.somewhere.com/blogroll to somewhere.com.

(defun jf/utility/maybe-url-domain-from-string (string)
  "Return domain from STRING when URL scheme present.

Else fallback to provided STRING"
  (require 's)
  (if (s-contains? "://" string)
      (s-join "."
              (cl-subseq
               (s-split "\\." (nth 2 (s-split "/" string))) -2))
    string))

Next was to figure out how to invoke this function. With a bit of searching, I stumbled upon Org mode how to sort entries - Emacs Stack Exchange.

With a bit of experimenting, I settled on the following function.

  (org-sort-entries
   nil
   ?f
   (lambda ()
     (let ((heading (nth 4 (org-heading-components))))
       (jf/utility/maybe-url-domain-from-string heading))))

Given that the function was relative to the page, I chose to add an elisp type link to the elfeed-org document, just below the heading that contains the sub-headings and all the feeds. Sidenote See External Links (The Org Manual) for documentation about the elisp format.

It looks like this:

[[elisp:(org-sort-entries nil ?f (lambda () (let ((heading (nth 4 (org-heading-components)))) (jf/utility/maybe-url-domain-from-string heading))))][Sort Headings!]]

Now my feed document has a link called Sort Headings!; and when I click it I’m prompted to confirm running the function, which then updates the sort order according to my whim.

-1:-- Quick and Dirty Function to Sort My Feed (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--February 24, 2024 06:07 PM

Jeremy Friesen: Update on the Campaign Status Document

We’ve completed six sessions of In the Shadows of Mont Brun . We’re using Errant 📖 for our campaign in the Savoy region of Europe.

In Test Driving a Campaign Status Document I wrote about my campaign note-taking. Since then, I’ve significantly refined the structure of the document.

I’ve settled on the following top-level headings:

  • Player Characters
  • Non-Player Characters
  • Sessions
  • Factions
  • Locations
  • Bestiary
  • Procedures
  • Rumours
  • Questions

Each heading then has topical sub-headings. Sidenote Nothing unique or overly interesting. And those sub-headings have further notes.

Benefits of the Lowly Outline

This document serves a multi-modal purpose; I use it when preparing my session, while running the session, and then later synthesizing the session.

And because of the outline form being written in Org-Mode 📖 , I have several views into the document. Headings support status custom states, like TODO or WAITING. I can also tag those sections and filter on the tags. Again, basic Org-Mode functionality.

Outline View

With a quick key stroke, I can open a side window to see the top two heading levels of the status document.

I use this functionality while I’m running a session. Taking a quick peek at names, as a reminder of who I might reintroduce.

I can also use the outline for navigation. Sidenote Though it is not the only means as I’ll soon explain.

Image

A screenshot of the base line status document.

Technical Implementation

My default outline depth in Org-Mode is 4. Sidenote (setq org-imenu-depth 4) But for the Status Document, I’ve set the outline depth to 2. Sidenote See 49.2.4 Local Variables in Files of the Emacs 📖 documentation.

I’ve bound the command imenu-list-smart-toggle to Cmd + 4. Sidenote s-4 in Emacs syntax. And I’ve added the document local variable org-imenu-depth: 2.

Heading Navigation

The heading navigation functionality relates to the Outline View functionality, but instead of seeing the full outline, I have a completion prompt.

I start typing and completion mechanism narrows those candidates. I can type Return to jump to that candidate.

I find that while I’m running the game and referencing the document, this quick movement can help me lookup something.

Image

A screenshot of the outline menu.

Technical Implementation

This navigation leverages the org-imenu-depth to limit the candidates to the top two heading levels.

I’ve bound the consult-org-heading function to Option + g then o. Sidenote M-g o in Emacs syntax. Mentally mapping to the mnemonic of “Go to Outline.” In non Org-Mode documents, I bind consult-outline the same key combination.

From Consult’s documentation:

consult-org-heading: Variant of consult-imenu or consult-outline for Org buffers. The headline and its ancestors headlines are separated by slashes. Supports narrowing by heading level, priority and TODO keyword, as well as live preview and recursive editing.

I like the ability to see the greater context of a particular heading, which allows for different means of filtering.

I consider the Consult package to be a high-utility enhancement of Emacs default completing read functionality. Mixin the Orderless package, and I have a wonderful fuzzy narrowing capabilities.

The Lowly “Session Notes”

When I’m preparing for the next session, I add a sub-heading under “Sessions”. There I write my notes, create links to resources.

And while I’m running the session, it is here where I write down all of those notes.

While running I might use my random-tables Emacs package 📖 to generate a random encounter, a dice roll, a weather event, or much of anything. And all of those rolls are inserted into the document. In other words, my notes are also a custom application.

Image

Random table prompt with partial completion using Vertico and Orderless.

List of random tables I’ve transcribed into Emacs
  • Ask the Stars
  • Black Sword Hack > Attributes
  • Black Sword Hack > Demon Name
  • Black Sword Hack > How to find the demon you seek
  • Black Sword Hack > Nickname
  • Black Sword Hack > Oracle Event
  • Black Sword Hack > Oracle Question
  • Black Sword Hack > Travel Event
  • Character Attitudes
  • Corporation, Sci-Fi
  • Deity
  • Descriptive Words to Pair with Concrete Element
  • Errant > Archetype > Equipment
  • Errant > Character
  • Errant > Chase Developments
  • Errant > Conspicuous Consumption Failed Save
  • Errant > Death & Dying
  • Errant > Death & Dying > Burning
  • Errant > Death & Dying > Physical
  • Errant > Death & Dying > Shocking
  • Errant > Death & Dying > Toxic
  • Errant > Debt Holder
  • Errant > Downtime Event
  • Errant > Failed Professions
  • Errant > General Downtime Turn Action
  • Errant > General Event
  • Errant > Grimoire
  • Errant > Henchman
  • Errant > Keepsakes
  • Errant > Lock
  • Errant > Reaction Roll
  • Errant > Weather
  • Escape the Dungeon
  • Everything Is Not as Expected
  • HRE > Contact
  • HRE > Random Encounter > Urban
  • HRE > Random Encounter > Wetlands or Coastal
  • HRE > Random Encounter > Wilderness
  • Herbalist’s Primer > Plant
  • Heresy
  • How Far Away is a Thing > Distant Thing
  • How Far Away is a Thing > Same Place
  • In the Shadows of Mont Brun > In the stagecoach
  • In the Shadows of Mont Brun > Names
  • Inn Name
  • Intervention Type
  • Ironsworn > Action Oracle
  • Ironsworn > Theme Oracle
  • Knave > Divine Domain
  • Knave > Divine Symbol
  • Knave > Potion Effect
  • Knave > Wilderness Region
  • Knave > Wilderness Structure
  • Knave > Wilderness Theme
  • Knave > Wilderness Trait
  • Laws of the Land
  • Location Adjectives of Connection
  • Location Adjectives of Disconnection
  • Lore24 Insipiration
  • Mortal Site
  • Mythical Castle Region Encounter
  • Mythical Castle Region Fantastical Encounter
  • Name
  • Noble House
  • Noble House > Atriarch Age
  • OSE > Attributes > 3d6 Down the Line
  • OSE > Monster Motivation
  • OSE > Random Dungeon Content
  • OSE > Reaction Roll
  • OSE > Secondary Skill
  • Oblique Strategy
  • Oracle
  • Person
  • Plot Twist
  • Random NPC Quirks
  • Savoy Region > Random Encounter
  • Scarlet Heroes > NPC
  • Scarlet Heroes > Reaction Roll
  • The Anarchical Grimoire of Propylonic Discharges
  • The One Ring > Lore
  • The One Ring > Lore > Action
  • The One Ring > Lore > Aspect
  • The One Ring > Lore > Focus
  • What is the monster doing?”

Tabular Views

The above Outline View and Heading Navigation functions are things I’ve been using for quite some time. But the Tabular Views are something I stumbled upon recently and have since further explored.

With each top-level heading I declare a list of keywords that I want to present in a table. Then for each sub-heading I fill out those keywords.

For my Bestiary, the keywords are: Name, Threat (abbreviated as T) Hit Point (HP 📖) , Attacks, Movement Dice (MD 📖) , Morale Level (ML 📖) , and Alignment (abbreviated to AL).

I can then toggle to see the tabular view of those keywords for the sub-heading.

Meaning, I have a quick overview of my the Bestiary stat block.

Image

Viewing Properties as Tabular Data

Technical Implementation

For the Bestiary, I’ve declared the following in the Property Drawer:

:COLUMNS: %ITEM(NAME) %THREAT(T) %HP %ATTACKS %MD %ML %ALIGNMENT(AL)

The %ITEM column populates the table column with the sub-heading’s text. The %ALIGNMENT(AL) specifies the ALIGNMENT property but uses AL as the column heading. Sidenote Instead of using the NAME property, I could use %ITEM; this would use the heading’s text.

And then for each sub-heading, I add properties for each of those named elements.

Here is one for a human soldier:

** Soldier
:PROPERTIES:
:ID:       EBC67225-8194-470D-ABC2-29745390F7C0
:HP: 7
:ATTACKS: 1d8 weapon
:ML: 7
:ALIGNMENT: L
:END:

Further, while in the column view, I can edit the properties.

For the Sessions section, I track the Experience Points (XP 📖) earned. I store that as a property on each session, and specify that I want to see a sum of those values. A nice and lazy XP tracker.

Dynamic Table Blocks

Building on the tabular view, I’ve added three dynamic tables via the columnview declaration. Sidenote See Capturing column view (The Org Manual) documentation.

  • Player Characters
  • Sessions
  • Bestiary

These tables reuse the declared columns and show them inline. They are also part of the export, which I’ll get to later.

I use these “always visible” tables to provide quick summaries to help answer in the moment questions:

  • Where were they on a given date?
  • What are the Player Characters (PCs 📖) names?
  • What’s the monster stack block?
Image

Dynamic table view with properties

Technical Implementation

This is fairly straight forward, I add the following just below the property drawer:

#+BEGIN: columnview :hlines 1 :id local :skip-empty-rows t
#+END:

Then I can call org-dblock-update to populate the table. I also set an save hook to auto-update all tables in the file. Sidenote Here’s a link to Github for section of code for conditional table recalculation.

Code for Conditional Recalculation
;;; From https://emacs.stackexchange.com/questions/22210/auto-update-org-tables-before-each-export
;; Recalculate all org tables in the buffer when saving.
(defvar-local jf/org-enable-buffer-wide-recalculation t
  "When non-nil, recalculate all dynamic regions when saving the file.

This variable is buffer local.")
;; Mark `jf/org-enable-buffer-wide-recalculation' as a safe local
;; variable as long as its value is t or nil. That way you are not
;; prompted to add that to `safe-local-variable-values' in custom.el.
(put 'jf/org-enable-buffer-wide-recalculation
     'safe-local-variable #'Boolean)

(defun jf/org-recalculate-buffer-tables (&rest args)
  "Wrapper function for `org-table-recalculate-buffer-tables' and
`org-dblock-update' that runs that function only if
`jf/org-enable-buffer-wide-recalculation' is non-nil.

Also, this function has optional ARGS that is needed for any
function that is added to
`org-export-before-processing-hook'. This would be useful if this
function is ever added to that hook."
  (when jf/org-enable-buffer-wide-recalculation
    (progn
      (org-table-recalculate-buffer-tables)
      (org-dblock-update '(4)))))

(defun jf/org-recalculate-before-save ()
  "Recalculate dynamic buffer regions before saving."
  (add-hook 'before-save-hook
            #'jf/org-recalculate-buffer-tables nil :local))
(add-hook 'org-mode-hook #'jf/org-recalculate-before-save)

Then at the bottom of my status document I added the following to enable the recalculate on save:

# Local Variables:
# jf/org--enable-buffer-wide-recalculation: t
# END:

For awhile, I had been declaring each sub-heading as radio targets. Sidenote See Radio Targets (The Org Manual) for more documentation and details. This would automatically convert all textual references of the heading into links to that heading.

To declare a radio target in Org-Mode , I would prepend the target declaration with \<\<\< and append \>\>\>. I could then refresh the buffer and all instances of that word (except the target) would become links to that target.

What I discovered, however, is that when sub-heading’s text is a radio target, it would produce an empty %ITEM cell for that sub-heading. Before I knew of the %ITEM column name, I had set a :NAME: property.

I did spend an hour trying to specify the org-radio-target-regexp variable, to customize what would be considered a radio target. However, I wasn’t able to make the Regular Expression (RegEx 📖) work.

Capture Templates

When I’m running a session, I often create Non-Player Characters (NPCs 📖) on the fly. I grab a name, a quirk, and a motivation. And from there run with it.

Given that I use the status document while I’m running a session, I wanted a way to randomly generate an NPC .

I wrote an Org-Mode capture template to perform this task. Sidenote See Capture (The Org Manual) for documentation. I’ve set it up, so that I type a few keys and presto I buffer that shows me this new NPC . I can, in the moment, take notes or refine the NPC .

When complete, I save the buffer and it is inserted into my status document as a sub-heading of the Non-Player Character heading.

As an added bonus, I don’t need to be editing my status document to create an NPC . When the mood strikes, I can start a capture by typing Control+c then c to bring up the capture menu.

Technical Implementation

First, let’s look to the capture template definition. Sidenote See my Org Mode configuration on Github.

("n" "NPC"
 entry (file+headline jf/campaign/file-name "Non-Player Characters")
 "* %(jf/campaign/random-npc-as-entry)\n%?"
 :empty-lines-after 1
 :jump-to-captured t)
  • n is the template key.
  • “NPC” is the label of the template.
  • The entry declaration means to write this as a heading in Org-Mode .
  • (file+headline jf/campaign/file-name "Non-Player Characters") means to file the new entry in the current campaign file-name and as a sub-head of the “Non-Player Characters” heading.
  • "* %(jf/campaign/random-npc-as-entry)\n%?" is the template, in this case a headline with most of the body created by my jf/campaign/random-npc-as-entry function. More on that in a bit.

The remaining keywords add an empty line afterwards and once I’m done “capturing” this information, Emacs will jump to that file and sub-heading.

The jf/campaign/random-npc-as-entry function.
(defun jf/campaign/random-npc-as-entry ()
  "Create an NPC entry."
  (let* ((random-table/reporter
          ;; Bind a function that will only output the results of the
          ;; table, excluding the expression that generated the results.
          (lambda (expression results) (format "%s" results)))
         (name
          (random-table/roll "In the Shadows of Mont Brun > Names"))
         (quirk
          (random-table/roll "Random NPC Quirks"))
         (alignment
          (random-table/roll "Noble House > Alignment"))
         (lore-table
          (random-table/roll "The One Ring > Lore"))
         (locations
          (s-join ", "
                  (completing-read-multiple
                   "Location(s): "
                   (jf/campaign/named-element :property "NAME"
                                              :tag "locations"))))
         (factions
          (s-join ", "
                  (completing-read-multiple
                   "Faction(s): "
                   (jf/campaign/named-element :property "NAME"
                                              :tag "factions")))))
    (format (concat
             "<<<%s>>>\n:PROPERTIES:\n:NAME:  %s\n:BACKGROUND:\n"
             ":LOCATIONS:  %s\n:DEMEANOR:\n:ALIGNMENT:  %s\n"
             ":QUIRKS:  %s\n:FACTIONS:  %s\n:END:\n\n%s"))
    name name locations alignment quirk factions lore-table))

Using my random-tables Emacs package I randomly create a name, quirks, alignment, and an oracle of action, aspect, and focus. I then use the campaign document to conditionally prompt for relevant locations and factions for the character.

There are more details but those are left as an exercise for the reader.

Exporting

As everything is in an Org-Mode document, I can export to multiple formats; in particular PDF 📖 via LaTeX 📖 .

The export recalculates the dynamic tables. It also exports, as definitions, the drawer properties. Meaning these bits of metadata appear as definition lists of term and value.

With Org-Mode , I can also tag headings as ones to skip for export. There are some secrets within my document that are not part of the export.

Here’s a link to my somewhat redacted session notes.

Technical Implementation

To achieve the layout, I use the following LaTeX headers and declarations. Sidenote See my Org-Mode include file for this LaTeX configuration. I then include the file in my document. Sidenote This LaTeX configuration will automatically start and stop the two-column functions.

LaTeX class and header declarations.
#+LATEX_CLASS: jf/article
#+LATEX_CLASS_OPTIONS: [11pt]
#+LATEX_HEADER: \usepackage[linktocpage=true]{hyperref}
#+LATEX_HEADER: \usepackage[french,english]{babel}
#+LATEX_HEADER: \usepackage[a4paper,top=3cm,bottom=3cm]{geometry}
#+LATEX_HEADER: \usepackage{minimalist}
#+LATEX_HEADER: \usepackage{fontspec}
#+LATEX_HEADER: \usepackage{caption} \captionsetup{labelfont=bf,font={sf,small}}
#+LATEX_HEADER: \setmainfont{TeX Gyre Pagella}
#+LATEX_HEADER: \usepackage{enumitem} \setlist{nosep}
#+LATEX_HEADER: \usepackage{longtable}
#+LATEX_HEADER: \usepackage{microtype}
#+LATEX_HEADER: \AtBeginEnvironment{longtable}{\footnotesize}
#+LATEX_HEADER: \usepackage[marginal,hang]{footmisc}
#+LATEX_HEADER: \usepackage{relsize,etoolbox}
#+LATEX_HEADER: \AtBeginEnvironment{quote}{\smaller}
#+LATEX_HEADER: \AtBeginEnvironment{tabular}{\smaller}
#+LATEX_HEADER: \usepackage[printonlyused,nohyperlinks]{acronym}
#+LATEX_HEADER: \usepackage[marginal,hang]{footmisc}
#+LATEX_HEADER: \usepackage{multicol}
#+LATEX_HEADER: \setlength\columnsep{20pt}
#+LATEX_HEADER: \hypersetup{colorlinks=true, linkcolor=blue, filecolor=magenta, urlcolor=cyan}
#+LATEX_HEADER: \tolerance=1000
#+LATEX_HEADER: \usepackage{float}
#+LATEX_HEADER: \usepackage{rotating}
#+LATEX_HEADER: \usepackage{sectsty}
#+LATEX_HEADER: \usepackage{titlesec}
#+LATEX_HEADER: \titleformat{\section}{\normalfont\fontsize{12}{18}\bfseries}{\thesection}{1em}{}
#+LATEX_HEADER: \setcounter{secnumdepth}{1}

# If this is a header; it will affect the table of contents.  But as a LATEX
# command, it happens in the processing order of the org file.
#+LATEX: \let\oldsection\section
#+LATEX: \renewcommand{\section}[1]{\newpage\end{multicols}\oldsection{#1}\begin{multicols}{2}}

For each of the dynamic tables, I end the multi-column layout before rendering the table and then resume the multi-column layout after the table; by adding the line #+LATEX: \end{multicols} before the table and adding #+LATEX: \begin{multicols}{2} after the table.

I had also been encountering issues where the generator would not render a table of contents. But based on some online guidance, I circumvented the problem by processing the .tex file twice.

Here’s the configuration for that:

(setq org-latex-pdf-process
        '("lualatex -shell-escape -interaction nonstopmode -output-directory %o %f"
          "lualatex -shell-escape -interaction nonstopmode -output-directory %o %f"))

I have specified that I want to export the following drawer properties:

And when I export drawer properties, I want them to be exported as description lists. I also only want the text of the headline exported, not it’s tags nor status.

Code for refining export.
;; Convert the data ":PROPERTY: VALUE" into a latex item or markdown
;; definition term and detail.
(setq org-export-filter-node-property-functions
  '(jf/ox/transform-node-property-to-item))

(defun jf/ox/transform-node-property-to-item (data back-end channel)
  "Convert DATA to appropriate markup for given BACK-END.

CHANNEL is ignored."
  (let* ((field-value (s-split ":" data))
          (term (s-titleize (s-replace "_" " " (car field-value))))
          (value (s-trim (cadr field-value))))
    (if (s-blank? value)
      ""
      (cond
        ((eq back-end 'latex)
          (format "\\item[{%s:}] %s\n" term value))
        ((eq back-end 'md)
          (format "%s\n: %s\n" term value))
        (t data)))))

(defun jf/org-latex-property-drawer (_property-drawer contents _info)
  "Transcode a PROPERTY-DRAWER element from Org to LaTeX.
CONTENTS holds the contents of the drawer.  INFO is a plist
holding contextual information."
  (and (org-string-nw-p contents)
       (format
        "\\begin{description}\n%s\\end{description}\n\\vspace{5mm}"
        contents)))

(advice-add #'org-latex-property-drawer
            :override #'jf/org-latex-property-drawer)

(defun jf/org-latex-format-basic-headline-function
  (_todo _todo-type _priority text _tags _info)
  "Only render the TEXT of the headline.
See `org-latex-format-headline-function' for details."
  text)

Here’s a copy of my Status Document exported to a PDF.

Conclusion

The structure of Org-Mode paired with various Emacs functions creates a source of record that flexes with different uses for the same document. Now my prep, session notes, and “world-building” all exist within a single document.

-1:-- Update on the Campaign Status Document (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--February 24, 2024 01:43 AM

Jeremy Friesen: Renaming Files Using Denote Schema in a Dired Buffer

I wrote Creating an Emacs Command to Rename Files per Denote File Naming Schema. I’ve been using it in a one-off situation.

And I wrote a wrapping function to call in dired.

(defun jf/dired-rename-files-to-denote-schema ()
  "Rename marked files in `dired-mode'."
  (interactive)
  (when (seq-find (lambda (file)
                    (member (file-name-nondirectory file) '("." "..")))
                  (dired-get-marked-files))
    (user-error "Can't rename \".\" or \"..\" files"))
  (dolist (file (dired-get-marked-files))
    (let ((current-prefix-arg nil))
      (apply #'jf/rename-file-to-denote-schema
        (list :file file :signature :prompt)))))

The above did require a bit of a refinement to the initial implementation. The refined jf/rename-file-to-denote-schema is available here.

What all of this means is that I can go to a directory, mark the files I want to rename (typing m on the row), then invoking M-x jf/dired-rename-files-to-denote-schema.

This also demonstrates the utility and flexibility of separating the looping logic from the logic for performing an action.

-1:-- Renaming Files Using Denote Schema in a Dired Buffer (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--February 06, 2024 11:06 PM

Jeremy Friesen: Adjusting My Time Tracking Again

Background

My day job involves tracking times towards different projects. When I first started that job in , we were to report our hours at the end of the month. Then, to improve our budgeting and forecasting, we shifted to weekly reporting.

What this has meant is that my needs for time tracking have changed. And in using these systems, so has my understanding.

, I worked out a new approach.

The prior approach was as follows:

  • Each project/client has a separate Org-Mode 📖 document.
  • When I start working on a task, I select from an existing project, then select or create a new task, and start recording time to that task.

The idea of this prior setup is that tasks might span multiple days. And if I was working on a task for multiple days, I might have notes for those tasks. Sidenote I could also see how long I was spending on a task; but that was not information we were capturing in our time reporting. And frankly, I’m thankful because tracking effort at a task level is arduous and requires both discipline and clear demarcation of tasks. Put another way, I want to spend the least mental effort thinking “Where do I put this measure of time.” Especially when I switch between inter-dependent tasks on the same project.

Enter Trepidation

I have used the prior approach for since . What I found was that I was fighting with which headings should have the clock.

I was also spending just a bit of time choosing which task; or writing a new one. Sidenote Nothing significant, but these little decision cuts add up. And there was a matter of closing out those todo items.

The reality was that there was a misalignment in regards to what I needed to track and the mental energy for managing a more robust time and note tracking system.

Considering Alternatives

I found myself thinking to the original time tracking; one file for the month. And how I had minimal decision making. Sidenote I also had a less robust Personal Knowledge Management (PKM 📖) .

I reviewed Denote 📖 ’s “Keeping a Journal or Diary”, and decided to take the plunge into using a daily note taking.

Why dailies?

  • It is similar to the original format that I found worked well.
  • I’ve learned a few things to make it easier to enter and review my time; I’ll get to that in a bit.
  • I’ll spend less time searching for the right task for the day; I tend to jump back to a task within the scope of a day.

Also because of my Org-Mode date link, I can incorporate those dailies in a buffer linking those dates. Sidenote See my Denote configuration for date links.

Let’s Get to Some Code

The Capture Process

("t" "Task (via Journal)"
 entry (function denote-journal-extras-new-or-existing-entry)
 "* %^{Task} :%(jf/project-as-tag):\n\n- Link to Project :: %(jf/project-as-link)\n\n%?"
 :empty-lines-before 1
 :empty-lines-after 1
 :clock-in t
 :clock-keep t
 :jump-to-captured t)

I’m relying on the denote-journal-extras-new-or-existing-entry function from Denote to do the heavy lifting of selecting files. I configured the denote-journal-extras-title-format to use 'day-date-month-year. Sidenote Which produces a title of Wednesday 31 January 2024.

The capture template also starts tracking time.

The template (e.g. "* %^{Task} :%(jf/project-as-tag):\n\n- Link to Project :: %(jf/project-as-link)\n\n%?") does a few things let’s break this down.

  • Prompt for naming the text;
  • Prompt for one of my projects and write it as a tag.
  • A bit of plain text
  • From the prompted project, create a link to that project.
  • The place where I start writing my notes.

Prompting for the project re-uses existing code, and memoizes a link to the project. Sidenote Due to the timing of the template evaluation, I wasn’t able to use the kill-ring.

(defun jf/project-as-tag ()
  "Prompt for project and kill link to project."
  (let* ((project
          (completing-read "Project: " (jf/project/list-projects)))
         (keyword
          (denote-sluggify-keyword project))
         (file
          (cdar (jf/project/list-projects :project project)))
         (identifier (denote-retrieve-filename-identifier file)))
    (setq jf/link-to-project (format "[[denote:%s][%s]]" identifier project))
    keyword))

And jf/project-as-link simply cleans up on itself.

(defun jf/project-as-link ()
  (let ((link jf/link-to-project))
    (setq jf/link-to-project nil)
    link))

Incorporating into my Agenda

To leverage Org-Mode ’s agenda report, I need to add the new file to my agenda. I do this by leveraging the denote-journal-extras-hook, which is called each time I create a new journal entry. Sidenote Using (add-hook 'denote-journal-extras-hook #'jf/org-mode/agenda-files-update).

We requiry for the agenda files with jf/org-mode/agenda-files.

(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)) (jf/project/list-projects))))
    (dolist (file (jf/journal/list-current-journals))
      (setq projects (cons file projects)))
    (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)))
    projects))

To get the daily journals, I leverage the fd 📖 command line tool, and look for the latest 14 journal entries that I’ve created.

(defun jf/journal/list-current-journals ()
  "Return the last 14 daily journal entries."
  (split-string-and-unquote
   (shell-command-to-string
    (concat
     "fd _journal --absolute-path "
     denote-journal-extras-directory " | sort | tail -14"))
   "\n"))

Wrapping Up

I’m looking at my processes as mutable. Paying attention to what is “working” for me and what’s creating mental friction. And moving away from that friction.

Along the way of changing my process, I learned a few things and figured they were worth sharing.

-1:-- Adjusting My Time Tracking Again (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--February 01, 2024 12:59 AM

Jeremy Friesen: Creating an Emacs Command to Rename Files per Denote File Naming Schema

I’ve been using Denote 📖 for creating and linking to notes. I appreciate the Denote naming schema: IDENTIFIER==SIGNATURE−−TITLE__KEYWORDS.extension.

For example the file name for this blog post is: 20240123T232919--creating-an-emacs-command-to-rename-files-per-denote-file-naming-schema__emacs_programming.org. I have not assigned a signature to this post. Sidenote See Leveraging Denote's Signature for Multiple Purposes for how I use signatures.

  • 20240123T232919 is the IDENTIFIER.
  • -creating-an-emacs-command-to-rename-files-per-denote-file-naming-schema is the title converted to a slug friendly format.
  • _emacs_programming are the two keywoards for this post.

I started naming other non-Denote files using this convention. But it’s a bit error prone.

So I spent time writing an Emacs 📖 function to help automate renaming file.

(cl-defun jf/rename-file-to-denote-schema (&key
                                           (file (buffer-file-name))
                                           dir id title keywords
                                           date signature
                                           force dry-run)
  "Rename FILE using `denote' schema.

When no FILE is provided use `buffer-file-name'.

- DIR: target directory to move the file to; by default put the
       new file in the same directory.

- ID: the identifier for this file, defaulting to one created via
      `denote-create-unique-file-identifier'.

- TITLE: The tile for this file; default's to a
         `s-titleized-words' of the given FILE's base name.

- KEYWORDS: A list of keywords to apply to the file.  When passed
            :none, skip prompting, via `denote-keywords-prompt'
            for a list of keywords.

- SIGNATURE: The optional signature of the file.  When passed
             :none, skip prompting for a signature.

- DATE: When non-nil, use this date for creating the ID via
        `denote-create-unique-file-identifier'.

- FORCE: When non-nil, rename the file without prompting to
         confirm the change.

- DRY-RUN: When non-nil, do not perform the name change but
           instead message the file's new name."
  (interactive)
  (let* ((title
          (or title (read-string "Title: "
                                 (s-titleized-words (f-base file)))))
         (id
          (or id (denote-create-unique-file-identifier
                  file (denote--get-all-used-ids) date)))
         (keywords
          (if (equal keywords :none)
              '()
            (or keywords (denote-keywords-prompt))))
         (signature
          (if (equal signature :none)
              ""
            (or signature
                (completing-read "Signature:" (jf/tor-series-list)))))
         (dir
          (f-join (or dir (f-dirname file)) "./"))
         (extension
          (f-ext file t))
         (new-name (denote-format-file-name
                    dir id keywords
                    title extension signature)))
    (if dry-run
        (message "Changing %S to %S" filename new-name)
      (when (or force (denote-rename-file-prompt file new-name))
        (denote-rename-file-and-buffer file new-name)
        (denote-update-dired-buffers)))
    new-name))

Here’s one example:

(jf/rename-file-to-denote-schema
 :file "~/git/dotemacs/README.org"
 :dir "~/git/archive"
 :title "Former Readme of Dotemacs"
 :keywords '("emacs" "programming")
 :signature :none
 :force t)

This would create a new file ~/git/archive/IDENTIFIER–former-reademe-for-dotemacs__emacs_programming.org and remove the old README.org. The IDENTIFIER would be a date time string.

Wrap-up

The Denote package comes with many small functions that work towards greater composition. The documentation is top-notch and the code is quite approachable.

The end result is that I now have a consistent mechanism for renaming these files; most importantly having a somewhat controlled vocabulary regarding my keywords, signatures, and all around file naming convention.

Cheers!

-1:-- Creating an Emacs Command to Rename Files per Denote File Naming Schema (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 24, 2024 04:58 AM

Jeremy Friesen: Advising Denote Function for Removing Diacritics from String

update: Adam Bark posted a refinement Removing diacritics from a string. He removes the need for a mapping of text. I’d definitely say that Bark’s method is more complete.

I write in English, a language now devoid of diacritics. In writing for my In-the-Shadows-of-Mont-Brun campaign, some of my post titles include a diacritic (e.g. Saint Egréve). The hiccup I encountered was that the diacritic would make it into the slug of the file as well as the slug of the URL 📖 .

These weren’t breaking problems, but my preference is to remove the diacritics from the slug and keep the diacritic in the title. This makes the slug easier to type.

I didn’t find a native Emacs 📖 function to strip diacritics but found the following:

Given the string ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž you could simplify this to AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz.

What I wanted was to convert these two strings into a list of cons cells; the thought being it’s easier to see the mapping.

I wrote the following function to zip the two strings into a single list.

(let ((diacritics
       (s-split "" "ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž" t))
      (simplified
       (s-split "" "AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz" t))
      (the-map
       '()))
  (-each-indexed diacritics
    (lambda (index diacritic)
      (add-to-list 'the-map (cons diacritic (nth index simplified)))))
  (end-of-buffer)
  (insert (format "\n%S" the-map)))

I then took inserted text and converted it into a variable:

(defvar jf/diacritics-to-non-diacritics-map
  '(("ž" . "z") ("Ž" . "Z")
     ("ý" . "y") ("ÿ" . "y") ("Ÿ" . "Y")
     ("š" . "s") ("Š" . "S")
     ("ñ" . "n") ("Ñ" . "N")
     ("ü" . "u") ("û" . "u") ("ú" . "u") ("ù" . "u")
     ("Ü" . "U") ("Û" . "U") ("Ú" . "U") ("Ù" . "U")
     ("ï" . "i") ("î" . "i") ("í" . "i") ("ì" . "i")
     ("Ï" . "I") ("Î" . "I") ("Í" . "I") ("Ì" . "I")
     ("Ð" . "D")
     ("ç" . "c") ("Ç" . "C")
     ("ð" . "e") ("ë" . "e") ("ê" . "e") ("é" . "e") ("è" . "e")
     ("Ë" . "E") ("Ê" . "E") ("É" . "E") ("È" . "E")
     ("ø" . "o") ("ö" . "o") ("õ" . "o") ("ô" . "o") ("ó" . "o") ("ò" . "o")
     ("Ø" . "O") ("Ö" . "O") ("Õ" . "O") ("Ô" . "O") ("Ó" . "O") ("Ò" . "O")
     ("å" . "a") ("ä" . "a") ("ã" . "a") ("â" . "a") ("á" . "a") ("à" . "a")
     ("Å" . "A") ("Ä" . "A") ("Ã" . "A") ("Â" . "A") ("Á" . "A") ("À" . "A"))
    "Map of diacritic to non-diacritic form.")

From this, I would have an easy time leveraging cl-reduce to convert the string. I wrote the jf/remove-diacritics-from function to encapsulate the behavior:

(defun jf/remove-diacritics-from (string)
  "Remove the diacritics from STRING."
  (cl-reduce (lambda (text diacritic-map-element)
               (s-replace (car diacritic-map-element)
                          (cdr diacritic-map-element) text))
             jf/diacritics-to-non-diacritics-map
             :initial-value string))

Last was to add advising functions to the various Denote 📖 sluggify functions. See below:

(defun jf/denote-sluggify (args)
  "Coerce the `car' of ARGS for slugification."
  (remove nil (list (jf/remove-diacritics-from
                     (s-replace "=" "_" (s-replace "-" "_" (car args))))
                    (cdr args))))
(advice-add #'denote-sluggify-signature :filter-args #'jf/denote-sluggify)
(advice-add #'denote-sluggify :filter-args #'jf/denote-sluggify)
(advice-add #'denote-sluggify-and-join :filter-args #'jf/denote-sluggify)
(advice-add #'denote-sluggify-keywords :filter-args #'jf/denote-sluggify)

I hope that this might be a useful reference for folks encountering this “hiccup.” I also assume that there might be a configuration point in Denote that would perhaps make this easier.

-1:-- Advising Denote Function for Removing Diacritics from String (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 07, 2024 06:11 PM

Jeremy Friesen: Leveraging Denote's Signature for Multiple Purposes

I’ve been working on tidying up my Personal Knowledge Management (PKM 📖) . In particular cleaning up my tags, both the number of tags and how I write the tags.

The number of tags is a side-effect of how all I had tagged entries:

Implementation Constraints of Tags

Let’s walk through the constraints of tags with consideration of:

Org-Mode

Org-Mode has native support for tags. Sidenote See Tags (The Org Manual). The tags work best as “one” word or words separated by the underscore (e.g. _).

According to Org-Mode ’s parsing logic examples of valid tags are:

  • notes
  • longrunonword (e.g. “long run on word” with the spaces removed)
  • long_run_on_word
  • LongRunOnWord (e.g. camel-case)

A valid tag should not have a dash (e.g. -).

To get the best out of Org-Mode , I wanted to migrate my tags to a conformant format. I also wanted to consider how Denote treats tags.

Anatomy of a Denote Entry

From Denote ’s documentation regarding file-naming scheme:

Every note produced by Denote follows this pattern by default (Points of entry):

DATE--TITLE__KEYWORDS.EXTENSION

As an optional extension to the above, file names can include a string of alphanumeric characters in the SIGNATURE field.

DATE==SIGNATURE--TITLE__KEYWORDS.EXTENSION

DATE
A unique identifier that is the year, month, day, and time of the note creation.
TITLE
The document’s title, with spaces replaced by dashes (e.g. -).
KEYWORDS
tags of the document. Each tag is separated with an underscore.
SIGNATURE
a user reserved slot; each word separated by an equal sign (e.g. =).

Important in this implementation is that if you have an Org-Mode tag of the from long_run_on_word, you’ll have a file with KEYWORDS slot of __long_run_on_word. For selecting existing keywords, Denote then treats the long_run_on_word as the following keywords: “long”, “run”, “on”, and “word”.

Put another way, in leveraging Denote , I want to avoid the tags that include the underscore (e.g. _). Sidenote There might be Denote configuration options I could explore, but that is perhaps a distraction. Or favor single word tags. Sidenote I did read that Denote allows you to Control the letter casing of file names. However I’m not quite there; perhaps in another round of updates.

Accessibility of Tags

The “long run on word” format for a tag is not one that I want to publish on my site. Screen readers don’t know how to parse those words, and frankly they are generally illegible.

When publishing, remember that #longrunonword is illegible. Favor #LongRunOnWord or #long_run_on_word or #long-run-on-word. The all-on-word hash tag is hard to parse, both for sighted readers and screen readers.

For those looking on my site, my multi-word tags are word separated by a dash (e.g. -). And to avoid lots of redirects, I’d like to keep the dash separated structure.

Back to the Tag Soup

My series tend to be multi-word (e.g. in-the-shadows-of-mont-brun). To be a conformant tag, I’d need to:

  • write it as a run-on word
  • replace those dashes with underscores
  • write it as camel-case

I chose to use tags for series so I could leverage Denote ’s Org dynamic blocks to insert links or backlinks.

My abbreviations tend to be singular words (e.g. css for Cascading Style Sheets, bx for Dungeons & Dragons Basic/Expert, etc.). These conform to the tag format, but the glossary entry for the abbreviation tends to be the only file with the tag.

I chose to have the abbreviations as part of the filename to provide easier lookup when I want to link to that concept.

Many of my tags are single words, but some are not. My preference would be a friendly multi-word format. But that introduces issues regarding Denote ’s exploding those multiple words into singular words.

Further, since my blog keeps tags and series separate; I’d prefer to keep those separate.

Leveraging Denote Signature

I had been following, in the background, the addition of Denote’s SIGNATURE. I had mentally framed it as an indexing strategy for Zettelkasten 📖 . However, reading more about it the signature is a user-reserved.

First, I wanted to remove tags which had only one file with the tag; the abbreviations.

Second, I wanted to separate the tag and series consideration. This aligns with the taxonomy of my blog.

Both series and abbreviations could leverage the SIGNATURE concept; though they would pollute the same space. To disambiguate, the abbreviation file would also have the abbr tag. So I could write a regular expression to separate abbreviation files from series files.

Both of the following scripts were easier to write because of the open and inspectable source Emacs 📖 .

Migrating the Abbreviations

, I migrated my abbreviations and series to use the SIGNATURE construct of Denote .

I used the following shell script to create a text file to work from:

fd _abbr -tf ~/git/org | rg "^[^=]*$" > working-file.txt

I then wrote up the following function:

Emacs function to convert abbreviations to a Denote SIGNATURE.
(bind-key "<f12>" #'jf/sign-denote-file)
(defun jf/sign-denote-file ()
  "Remove a keyword from the file; then sign with that keyword.

Each line is the text that is the path to the file."
  (interactive)
  (beginning-of-line)
  (let*
      ((beg (point))
       (end (progn (end-of-line) (point)))
       (file (buffer-substring-no-properties beg end)))
    (when (f-file-p file)
      (let* (
             ;; These are keywords I know I'm not going to select to
             ;; remove
             (keywords-to-skip
              '("abbr" "offer" "projects" "scientist" "game" "rpgs"
                "texteditors" "podcasts"))

             ;; I only work on org mode files.
             (file-type 'org)

             ;; The current keywords of the document
             (cur-keywords
              (denote-retrieve-keywords-value file file-type))

             ;; The keywords from which I want to select to remove from
             ;; the keyword list and then sign the file.
             (prompt-keywords
              (seq-difference cur-keywords keywords-to-skip #'string=))

             ;; The abbreviation we'll be removing
             (abbr
              (if
                  (= (length prompt-keywords) 1)
                  (car prompt-keywords)
                (completing-read "Keyword to Remove: "
                                 prompt-keywords nil t)))

             ;; The original title of the file.
             (title
              (denote-retrieve-title-value file file-type))

             ;; The original ID of the file.
             (id
              (denote-retrieve-filename-identifier file :no-error))
             (new-keywords
              (seq-uniq (seq-difference
                         cur-keywords
                         (-flatten (list abbr "offer")))
                        #'string=)))
        (when abbr
          (progn
            (denote-rewrite-keywords file new-keywords file-type)
            (let*
                ((sluggified-title
                  (denote-sluggify title 'title))
                 (keywords
                  (denote-retrieve-keywords-value file file-type))
                 (signature (denote-sluggify-signature abbr))
                 (extension
                  (denote-get-file-extension file))
                 (dir
                  (file-name-directory file))

                 ;; We'll be renaming the file based on the new metadata
                 (new-name
                  (denote-format-file-name
                   dir id keywords sluggified-title extension
                   signature)))
              (denote-rename-file-and-buffer file new-name)
              (denote-update-dired-buffers))))))))

I then mashed on F12 to work my way through that list. Ultimately converting about 600 entries.

Migrating the Series

As part of my blog writing, I had a simple function to prompt for a given series and insert that metadata in the blog post. By personal convention, each blog post only belongs to one series. Sidenote Though nothing prevents that in my Hugo 📖 setup.

I chose to extend that function so that when I add the series, I add the series as a SIGNATURE.

Below is the current implementation of that function:

Emacs function to add series to file.
(cl-defun jf/org-mode/add-series-to-file (&key file series drop-tags all)
  "Add SERIES to FILE.

Optionally DROP-TAGS, as there may have been a TAG associated
with the series."
  (interactive)
  (with-current-buffer (if file
                           (find-file-noselect file)
                         (current-buffer))
    (when (or all (jf/org-mode/blog-entry?))
      (let ((series
             (or series
                 (completing-read "Series: "
                                  (jf/tor-series-list) nil t))))
        (unless (and (jf/org-mode/blog-entry?)
                     (s-contains? "#+HUGO_CUSTOM_FRONT_MATTER: :series "
                                  (buffer-substring-no-properties
                                   (point-min) (point-max))))
          (save-excursion
            (goto-char (point-min))
            (re-search-forward "^$")
            (insert "\n#+HUGO_CUSTOM_FRONT_MATTER: :series " series)
            (save-buffer)))
        (let* ((file (buffer-file-name))
               (id
                (denote-retrieve-filename-identifier file :no-error))
               (file-type
                'org)
               (title
                (denote-retrieve-title-value file file-type))
               (sluggified-title
                (denote-sluggify (s-replace "…" "" title) 'title))
               (keywords
                (seq-difference
                 (denote-retrieve-keywords-value file file-type)
                 (-flatten drop-tags)))
               (signature (denote-sluggify-signature series))
               (extension (denote-get-file-extension file))
               (dir (file-name-directory file))
               (new-name (denote-format-file-name
                          dir id keywords sluggified-title
                          extension signature)))
          (denote-rename-file-and-buffer file new-name)
          (denote-update-dired-buffers))))))

I then grabbed all of my series, and for each series I ran the following to find each file that is part of the series.

fd "_SERIES(_|.org)\" ~/git/org/denote/"

I looped over each file, and called the jf/org-mode/add-series-to-file function. Between each series, I reviewed the output then committed the change.

Bi-Directional Considerations

As someone that regularly works with data, I want to consider the reversibility of a data migration. How easy will it be to roll back those changes?

Put another way, some data migrations lose information in their transformation. When I spent time mapping my prior tags to a new set of tags, I end up with many previous tags mapping to one tag.

Let’s pretend I had the tags “cat”, “dog”, and “horse”. And I mapped those all to “animal”. Once I’d completed the map, I would only have the “animal” tag.

I cannot easily reverse that mapping; at least not without keeping a ledger of which files had which original tags. Sidenote Which I did do. But have since discarded as I’ve now moved past what I consider the point of reversibility.

Conclusion

I love the design principle of Denote’s file naming convention. It exposes metadata at the file system level; allowing for tools such as fd 📖 and the venerable find to query your knowledge base.

The introduction of the signature, a user-reserved slug of the file name, provides the last bit of magic. I can keep my tags and series separate; and reduce the presence of tags that are only applicable to a single file.

And to reiterate, a future refactor is to move towards camel-case tags. Which would mean I could remove my mapping of run-on words tags to their dash separated form.

-1:-- Leveraging Denote's Signature for Multiple Purposes (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 06, 2024 07:04 PM

Jeremy Friesen: Current Workflow for Lore24 Writing

I’m participating in Lore24 📖 . Sidenote Follow along on my Lore24 tag. I’m both writing lore and developing my personal workflow. Sidenote I’m framing Lore24 as both a creative outlet but also a habit building endeavor.

I’m writing all lore in one document; conceptually a separate notebook. I may or may not write up a blog entry for each day. I’ll likely post each day’s lore to the Fediverse. Sidenote Follow me @dice.camp@takeonrules.

Three Steps for Lore24

  • Capture the Lore
  • Write a Blog Post
  • Post on the Fediverse

Capture the Lore

I am considering my Lore24 in the Shadows of Mont Brun Org-Mode 📖 document as the canonical place for this project.

To write an entry I use an Org-Mode capture template. Sidenote See Org Mode’s Capture Templates documentation. I start writing down my thoughts, maybe prompting for inspiration or pulling a card from Oblique Strategies 📖 . Sidenote See Preparation for Lore24.

Write a Blog Post

When the mood strikes, I’ll write a blog post expanding on each day’s lore. Sidenote This is analogous to how my poetry sometimes has further comments and other times is simply the poem. Expansions include commentary on the bit of “lore” as well as developing thoughts about future entries.

For the blog posts my preference is to avoid a hard-copy of the lore. I want to instead favor linking to the lore; yet the lore document remains a private document.

To achieve this goal, I’m leveraging the Org-Transclusion package, which allows me to write in one location, then add a “transclusion” element in another document, which in essence treats the text of the original location as though it were in the other document.

To facilitate this, I wrote an Emacs 📖 function: jf/create-lore-24-blog-entry. Sidenote See source code for jf/create-lore-24-blog-entry.

The Emacs function I wrote for creating a blog-post from my Lore24 entry.
(cl-defun jf/create-lore-24-blog-entry ()
  "Create #Lore24 entry from current node."
  (interactive)
  ;; Guard against running this on non- `jf/lore24-filename'.
  (unless (string=
            (jf/filename/tilde-based (buffer-file-name (current-buffer)))
            jf/lore24-filename)
    (user-error "You must be in %S" jf/lore24-filename))
  ;; Now that we know we're on the right buffer...
  (let* (
          ;; Get the node of the current entry I'm working from
          (node-id (org-id-get-create))

          ;; Prompt for a name.
          (name (read-string "#Lore24 Blog Post Name: "))

          ;; Determine the last blog post created.
          (previous-post-basename
            (s-trim (shell-command-to-string
                      (concat
                        "cd ~/git/org/denote/blog-posts; "
                        "find *--lore24-entry-* | sort | tail -1"))))

          ;; From the last blog post, derive the next index value.
          (next-index (format "%03d"
                        (+ 1
                          (string-to-number
                            (progn
                              (string-match
                                "--lore24-entry-\\([[:digit:]]+\\)-"
                                previous-post-basename)
                              (match-string-no-properties 1
                                previous-post-basename))))))

          ;; We must name the post.  "Lore 24 - Entry NNN: Name"
          (title (format "Lore24 - Entry %s: %s" next-index name ))

          ;; The body of the blog post; by default I leverage
          ;; `org-transclusion'.
          (template (format
                      (concat "#+HUGO_CUSTOM_FRONT_MATTER: :series %s"
                        "\n\n#+TRANSCLUDE: [[id:%s]] :only-contents "
                        ":exclude-elements \"drawer keyword headline\"")
                      "in-the-shadows-of-mont-brun"
                      node-id)))

    ;; Create the blog post
    (denote
      title
      '("lore24" "rpgs") ;; List of keywords
      'org ;; Format of the file
      (f-join (denote-directory) "blog-posts") ;; Sub-directory
      nil ;; Default to today's date
      template)))

I wrote the above function because twice I’ve failed to apply the wrong metadata to the blog post. By using the above function, I will create posts with a consistent filename format and metadata.

Post on the Fediverse

When I write a blog post, I’ll post to Mastodon the post’s URL 📖 and summary. When I don’t write a post, I’ll likely grab my notes and post in a format suitable for Mastodon.

Conclusion

Over the years, folks have encouraged me to write a book. I’ve had a few ideas, but haven’t set about doing so. In part because I didn’t know where to begin. Sidenote I write when the mood strikes; something that is at odds with completing a project.

I’m looking to the Lore24 project as a means of habitualizing writing towards a topic.

I want to reduce the duplication of knowledge. Sidenote Conform to the Don’t Repeat Yourself (DRY 📖) principle, if you will. I also want to explore means of bringing forth fragments of material and contextualizing those. Further, I want the “work” of Lore24 to be something that is easier to share.

-1:-- Current Workflow for Lore24 Writing (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 04, 2024 05:13 PM

Jeremy Friesen: Emacs Macros Continue to Amaze Me

I added my In-the-Shadows-of-Mont-Brun series to Take on Rules. I write in Org-Mode 📖 , and export to Markdown via the Ox-Hugo 📖 package. I then publish via Hugo 📖 . This means that I have two versions of documents:

  1. The original and canonical Org-Mode document.
  2. The exported and adjusted Markdown document.

I went through my Markdown to add the series metadata for each post. This involved navigating the next and previous links of posts tagged with “rpgs”; a quick way to review my posts.

I gathered the slugs of each of those posts, pasting them into a buffer. I then ensured that the two repositories started in a clean Git 📖 status. Sidenote My Personal Knowledge Management 📖 repository and my blog’s repository. Sidenote The clean Git status ensured that I could edit the results of the automation.

From that buffer, which I created within my blog’s project, I started recording a macro:

  • Goto the beginning of the line
  • Mark point
  • Go to end of the line
  • Copy the active region; this is the post’s slug
  • M-x to search for slug: and paste/yank the copied slug
  • Select the first candidate
  • Go to the top of the buffer
  • Then go to the next line (e.g. the second line of the buffer)
  • Type series: in-the-shadows-of-mont-brun
  • Save the buffer.
  • M-x jump to the related Org-Mode file (via jf/jump_to_corresponding_denote_file)
  • M-x add the series metadata to the org-mode file (via jf/org-mode/add-series)
  • Save the buffer.
  • Return to the working buffer with the slugs.
  • Move to the beginning of the next line.

That was my complete recording. Each slug required touching two separate files and returning to the checklist file.

From there, I typed Control+8 then F4. This called the last recorded macro (e.g. call-last-kbd-macro function) eight times. I then reviewed the Git

Put another way, taking the time to setup conditions for automation framed the task a bit as a game. A game of converting an otherwise repetitive, simple, and error prone task into something that could “happen in the background.”

And by ensuring that I started from a clean data state, I could proceed with a degree of confidence that I would not “permanently” mess anything up.

Emacs 📖 , you again amaze me with the simple power of the macro ecosystem. And I know I’ve only scratched the surface of the macro ecosystem’s capability.

For those curious about another case macro recording session, see my Using a TODO List and Keyboard Macros to Guide RSS Feed Review.

-1:-- Emacs Macros Continue to Amaze Me (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 03, 2024 08:04 PM

Jeremy Friesen: Amongst the Org-Mode Archipelago

As a writer, you withdraw and disconnect yourself from the world in order to connect to it in the far-reaching way that is other people elsewhere reading the words that came together in a contemplative state.
Rebecca Solnit, Orwell’s Roses

This morning the following rolled across my RSS 📖 feed:

The more I lean into using Org-mode files for everything, the more isolated I’m feeling. It may be irrational, because “plain text”, but having to export or otherwise translate everything when I post to my blog or other tools is becoming less fun. Org-mode Island is beautiful, but lonely.

I’m also one whom leans into Org-Mode 📖 for most everything I write. And then export to other formats as applicable.

Overview of my Org-Mode Usage

I’ve migrated my entire blog from being it’s own island; an island of Hugo structured markdown files once separate from my other writing (e.g. project notes, personal notes, SOPs 📖 , archived web pages for reference, etc.)

Now I write in Org-Mode knowing that I can transition notes from personal to public with minimal effort.

Some of my writing starts out very specifically as a blog post. Sidenote This current post, for example. While others start as me thinking, and ultimately deciding that these thoughts are ones that I can publish on my blog. Sidenote Preparation for Lore24 is one example.

In the case of my conference presentations, I start them in an Org-Mode document, either presenting directly from that document or transforming them to a suitable presentation format. Once I’ve completed my presentation, depending on the terms of that conference, I then export the document to my blog. Sidenote Mentoring VS-Coders as an Emacsian]] is one example.

In other cases, I’ll export the document to a collaborative tool; then include a link to that collaboration space in the originating document. And from that point forward, treat the collaboration space as the canonical location.

Brief Consideration of Other Options

I also consider that collaborative spaces require connectivity. Further, many of the tacitly agreed upon spaces of collaboration are available with the concession of allowing surveillance of your entire process.

And then there is the energy cost of those shared spaces. All of the telemetry and energy towards surveillance paired with the synchronization costs of sharing between multiple folk.

Think, Pair, Share Strategy

With all of that I don’t feel Jack’s isolation, but instead look to writing in Org-Mode as a quiet place to think. I consider the pedagogical strategy of “Think, Pair, Share.”

In which the educator will direct the class to:

  • First have students individually think/write some initial thoughts.
  • Then pair with someone to further develop each other’s thoughts.
  • And then to share with the group.

Conclusion

Org-Mode is my home hearth. Here I prepare my food. And sometimes I bring that food, wrapped up for transport, to a public place so that I might share. On a few occasions, I bring partially prepared ingredients and throw them in the collaboration soup; where we churn the ladel together.

I’m alone during my Org-Mode usage, but I don’t feel lonely in being here.

And even in this blog post, I’m not alone. I’m thinking of Jack’s writing as I measure out my response.

-1:-- Amongst the Org-Mode Archipelago (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 28, 2023 02:54 PM

Jeremy Friesen: Using a TODO List and Keyboard Macros to Guide RSS Feed Review

update: I spent a few sessions of focus working through my 167 RSS 📖 ; the checklist process meant that I could work through them until I felt decision fatigue set in. Then I could pick up where I left off.

in On Blogging and Online Neighborhoods. In that post I identified that I wanted to share my blog roll.

The Task at Hand

I started on that work and wanted to share a bit of the process.

  1. Normalize my current feed.
  2. Create a Todo oriented document.
  3. Do the Todo.
  4. Save my progress.

Normalize My Current Feed

I keep my blog-roll in an Org-Mode 📖 document; at ~/git/org/elfeed.org. Sidenote I used the elfeed-org package to store my feeds, which allows me allow me to annotate each feed. It was a bit unruly; several top-level headings and a smattering of tags.

What I wanted was a simple list of the URLs 📖 to the RSS feeds. That was relatively straight forward:

rg "^\*+\s+(http\S+)" ~/git/org/elfeed.org -r '** TODO $1' > ~/Desktop/normalized-rss-feed.org

The above Ripgrep 📖 command extracted the URLs as second level headings, with a status of Todo. Normally I would export these as checklists, but I knew I wanted to leverage Org-Mode ’s heading tags; because as I was reviewing each feed I wanted to categorize them. Sidenote The categories are useful for filtering my feed; if I’m searching for something.

Create a Todo Oriented Document

With the file created at ~/Desktop/normalized-rss-feed.org, I prepended a custom Todo sequence: TODO(t) STARTED(s) | PUBLIC(p) PRIVATE(r) DROP(d). Sidenote See TODO Basics (The Org Manual) for more details.

I envisioned the states as follows:

TODO
I still need to begin the work on this.
STARTED
I have opened the website and began assessing it.
PUBLIC
This is a feed that I will continue to subscribe to and share on my blog-roll.
PRIVATE
This is a feed that I will continue to subscribe to but will not share on my blog-roll.
DROP
This is a feed I will remove from my blog roll.

I added the STARTED to provide a placeholder for that moment when I begin looking at a website and could likely spend a few moments reading or jumping to other links.

I also added a first level headline above all of my second level headings. The [0/3] marker is a a tally of how many I’ve completed. Sidenote See Breaking Down Tasks (The Org Manual) for more details.

Below is an example of the document before I began the work of reviewing each feed.

#+TODO: TODO(t) STARTED(s) | PUBLIC(p) PRIVATE(r) DROP(d)

* Feeds [0/3]
** TODO http://ajroach42.com/feed.xml
** TODO http://decafbad.net/feed
** TODO http://evrl.com/feed.xml

The above document gave me a checklist of work to move through.

Do the TODO

With a checklist in hand, I thought about the steps I would take to open each page. With a game plan in mind, positioned my cursor at the beginning of the line of the first feed (e.g. ** TODO http://ajroach42.com/feed.xml).

What I wanted to do was to set the line to STARTED (e.g. “start” the task) and open the homepage for that feed.

Knowing that I would be doing this a lot, I started recording a macro and typed the following keys:

C-a C-a
Ensure we are at the beginning of the line; a good pre-amble for recorded macros.
C-c C-t s
Invoke org-todo and mark the headline as STARTED.
s-f / / <return>
Move the cursor to just after the first encountered //. We’re at the beginning of the host name.
M-b
Goto the beginning of the word, we’re not at the http point of the string.
C-SPC
Set the mark (e.g. begin the selecting text)
s-f / / <return> s-f / <return>
We are now highlighting the scheme and host name (e.g. http://decafbad.net/).
s-c
Copy the highlighted text; the homepage of the feed.
M-x e w w <return>
Invoke the eww command; the web browser within Emacs 📖 .
s-v <return>
Paste the copied homepage of the feed as the URL to visit.

Once I was done reviewing I would manually return to the STARTED item and again set it’s status to either PUBLIC, PRIVATE, or DROP. In setting the status the counter would increment.

Save My Progress

Because I was working from a checklist, this was a task that I could set down and pick up as I found moments in my workday.

Knowing this, I added a section to the document; namely the recorded macro exported:

(defalias 'review-elfeed
     (kmacro "C-a C-a C-c C-t s s-f / / <return> M-b C-SPC s-f / / <return> s-f / <return> s-c M-x e w w <return> s-v <return>"))

Now, when I come back to the document I can evaluate the block of text and have access to an a review-elfeed command.

Conclusion

This is the second time this week that I’ve used something similar to the above strategy; namely creating a checklist of Todo items; each of which require decisions and routing to different status.

The first time involved building a checklist of Github issues, gathering up all of the pull requests that went into resolving those issues, then tracking down each commit for those pull requests.

The second time, the one I write about in this post, was simpler to explain and more accessible to a general audience.

I appreciate the automation of Emacs ; it took awhile for me to use keyboard macros, but now I’m spoiled. I write them by first pushing the record button F3 and then replay them by pressing F4; and when I want to run them 100 times, I just type Ctrl 1 0 0 F4.

This is one reason I am trying to bring more and more of my computering to Emacs ; because the utilities I have available for writing, reading, and navigating text. Sidenote See Emacs Turbo-Charges My Writing.

When I first started recording keyboard macros, I tried to type as I normally would. But, what I found is that if I slow down, just a bit, I have a better chance of recording the correct sequence. Sidenote If I need to edit it, I can do that with edit-kbd-macro.

For those reading along, I hope this was helpful; and as always please feel free to reach out to me.

-1:-- Using a TODO List and Keyboard Macros to Guide RSS Feed Review (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 24, 2023 09:54 PM

Jeremy Friesen: Introducing Extensibility with a Macro, a List, and a Reducer

I’ve been working on my random-tables package; drawing inspiration from Howard Abrams’ EmacsConf 2023 talk “How I play TTRPGs in Emacs”. One concept that I knew I wanted was what I’ve named “inner tables”.

Drawing Inspiration from Others

Let’s say we have a simple table with two results:

Heads
A friendly [sprite/genie/ogre/nun] offers to help.
Tails
An angry [soldier/mutant/toad] is obstructing your way.

There are two “inner tables” (e.g. [spirit/genie/ogre/nun] and [soldier/mutant/toad]). And when we evaluate the result, we will pick one of those results; such that on a “Heads” result our text might say “A friendly ogre offers to help.”

I set about incorporating that concept into my random-tables package. At the point of integration I had a rather nasty case statement.

Original Case Statement
(if-let ((table (random-table/fetch text :allow_nil t)))
    (random-table/evaluate/table table roller-expression)
  (cond
   ((string-match random-table/roll/math-operation-regex text)
    (let* ((left-operand (match-string-no-properties 1 text))
           (operator (match-string-no-properties 2 text))
           (right-operand (match-string-no-properties 3 text)))
      ;; TODO: Should we be passing the roller expression?
      (funcall (intern operator)
               (string-to-number
                (random-table/roll/parse-text/replacer left-operand))
               (string-to-number
                (random-table/roll/parse-text/replacer right-operand)))))
   ((string-match random-table/roll/pass-roller-regex text)
    (let* ((table-name (match-string-no-properties 1 text))
           (roller-expression (match-string-no-properties 2 text)))
      (funcall (random-table/roll/parse-text/replacer
                table-name
                roller-expression))))
   ((and random-table/current-roll
         (string-match "current_roll" text))
    random-table/current-roll)
   (t
    ;; Ensure that we have a dice expression
    (if (string-match-p random-table/dice/regex (s-trim text))
        (format "%s" (random-table/dice/roll (s-trim text)))
      text))))

I initially added another case statement, but instead chose to refactor to a different approach. Further, the case statement had nested if statements; and implicit functions. All told, this bit of “magic code” was a lot to digest.

Refactor

The change was to go from a case statement to a list of functions that I passed to cl-reduce (see the below code):

(cl-reduce (lambda (string el) (funcall el string))
           random-table/text-replacer-functions
           :initial-value text)

The random-table/text-replacer-functions is a list of functions that each take one argument; namely a string. Each of those functions will return a string which will be passed to the next function in the list.

This refactor meant two things:

  1. I was defining a clear interface.
  2. The list was configurable; meaning extensible and perhaps easier to test it’s constituent parts.

Using a Macro to Conform to an Interface

Seeing that I had a common interface, I set about creating a macro to help produce conformant methods. Before I delve into the macro definition, let’s look at how I declared the “inner table” replacer function:

(random-table/create-text-replacer-function
 "Conditionally replace inner-table for TEXT.

Examples of inner-table are:

- \"[dog/cat/horse]\" (e.g. 3 entries)
- \"[hello world/good-bye mama jane]\" (2 entries)

This skips over inner tables that have one element (e.g. [one])."
 :name random-table/text-replacer-function/inner-table
 :regexp "\\[\\([^\]]+/[^\]]+\\)\\]"
 :replacer (lambda (matching-text inner-table)
             (seq-random-elt (s-split "/" inner-table))))

I use the “Conditionally replace…” as the docstring for the defined function.

The :name is the name of the function that I’ll explicitly add to the random-table/text-replacer-functions list.

The :regexp is the Emacs regular expression that finds the inner table.

The :replacer is a function that performs the replacement on a successful match of the regular expression.

The positional parameters of the :replacer function are the original test then each of the capture regions in the above expression.

Diving into the Macro

In earlier versions, I relied on the s-format function for evaluation. But that was inadequate in that it had a hard-coded regular expression. However, I used that source code for inspiration.

(cl-defmacro random-table/create-text-replacer-function
    (docstring &key name replacer regexp)
  "Create NAME function as a text REPLACER for REGEXP.

- NAME: A symbol naming the replacer function.
- REPLACER: A lambda with a number of args equal to one plus the
            number of capture regions of the REGEXP.  The first
            parameter is the original text, the rest are the
            capture regions of the REGEXP.
- REGEXP: The regular expression to test against the given text.
- DOCSTRING: The docstring for the newly created function.

This macro builds on the logic found in `s-format'"
  (let ((name (if (stringp name) (intern name) name)))
    `(defun ,name (text)
       ,docstring
       (let ((saved-match-data (match-data)))
         (unwind-protect
             (replace-regexp-in-string
              ,regexp
              (lambda (md)
                (let ((capture-region-text-list
                       ;; Convert the matched data results into a list,
                       ;; with the `car' being the original text and the
                       ;; `cdr' being a list of each capture region.
                       (mapcar (lambda (i) (match-string i md))
                               (number-sequence
                                0
                                (- (/ (length (match-data)) 2)
                                   1))))
                      (replacer-match-data (match-data)))
                  (unwind-protect
                      (let ((replaced-text
                             (cond
                              (t
                               (set-match-data saved-match-data)
                               (apply ,replacer
                                      capture-region-text-list)))))
                        (if replaced-text
                            (format "%s" replaced-text)
                          (signal 's-format-resolve md)))
                    (set-match-data replacer-match-data))))
              text t t)
           (set-match-data saved-match-data))))))

In the above macro the ,regexp replaces the hard-coded regexp of s-format (and is provided by the :regexp named parameter). I also removed quite a bit of logic, but needed to introduce extracting positional arguments to then pass to the :replacer function.

The code for creating the list of positional arguments is buried above, but I present it below for further discussion:

(let ((capture-region-text-list
       (mapcar (lambda (i) (match-string i md))
               (number-sequence 0 (- (/ (length (match-data)) 2) 1))))))

In the above code fragment, given that we have a :regexp hit, the (match-data) is a list that has 2N elements, where N is 1 plus the number of capture regions of the regular expression. The first two elements of the list are the beginning and ending character positions of the entire string. The next two elements are the beginning and ending character positions of the text that matches the capture; and so on.

I map the matches to a list of values, which are then passed as positional parameters via (apply ,replacer capture-region-text-list); in where the ,replacer is specified as the :replacer named parameter of the random-table/create-text-replacer-function macro call.

The List

Below is the declaration of the random-table/text-replacer-functions:

(defcustom random-table/text-replacer-functions
  '(random-table/text-replacer-function/current-roll
    random-table/text-replacer-function/dice-expression
    random-table/text-replacer-function/from-interactive-prompt
    random-table/text-replacer-function/named-table
    random-table/text-replacer-function/inner-table
    random-table/text-replacer-function/table-math)
  "Functions that each have one positional string parameter returning a string.

The function is responsible for finding and replacing any matches
within the text.

See `random-table/create-text-replacer-function' macro for creating one of these
functions."
  :group 'random-table
  :package-version '(random-table . "0.4.0")
  :type '(list :value-type (group function)))

The list helps document how those functions are expected to behave.

Conclusion

This weekend, I spent a handful of hours on the refactor. The goal of the refactor was to introduce handling of an “inner table”. The result was that I added new functionality and created a crease for further extensibility.

Another side-effect of the interface is that I can easily document, at the function definition level, the constituent parts and can explain how those parts fit together.

-1:-- Introducing Extensibility with a Macro, a List, and a Reducer (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 17, 2023 07:06 PM

Jeremy Friesen: Test Driving a Campaign Status Document

update: Added content disclaimer for Justin Alexander and removing link-back to his site.

I’m maintaining a Campaign Status Document. Sidenote In Emacs 📖 of course. . I learned of the Campaign Status Document in Justin Alexander’s So You Want to be a Game Master 📖 .

I have the following top-level headlines:

  • Persona Gratis: Player Characters (PCs 📖) and Non-Player Characters (NPCs 📖) .
  • Sessions: Notes for each session.
  • Procedures: Random encounters and local phenomenon by region.
  • Clocks: Events in motion.
  • Scenario Updates: Tracking changes to any published material.
  • Timeline Bangs: Things that could come into play; often with questions.
  • Background Events: Notes on things that have happened; often with questions about what it means.

I’ve written up session reports. Sidenote Which I enjoy writing. But these are not as useful for sustaining long-term campaigns; the long-term information remains scattered across many documents. The status document provides a framework; a framework that jolted me out of my previous patterns for role-playing game note taking.

Which is a bit of a tangent, but highlights a trapping of routine. Namely, I’ve run lots of games, taking notes on many of them; writing session reports on some of them. Reading So You Want to be a Game Master helped me introspect on my approach; one that was partially informed by How to Take Smart Notes. Sidenote The Campaign Status Document is a verbose index document. Critical to it’s utility is updating and archiving it.

Diving into the Document

The above is an expansion of the original, but it is working for me; in part because of the different views and actions I can take.

  • Outline
  • Random dice roller
  • Jump to PDFs or URLs
  • Overview of Characters

Outline

I use Org-Mode 📖 for my writing. When I’m on a headline, I can “tab” through opening and closing headlines: show only top-level, show all headlines, show all content.

I also make use of the imenu package; I can have a second panel open showing me a navigable overview.

I can also type a hot key to bring up a searchable menu of headlines, select one and go there.

In other words, I can quickly move around a growing document.

Random Dice Roller

I have written up a random-table.el an “Emacs package to provide means of registering and rolling on random tables.” This means I have quick access in my note taking utility to roll up and insert randomly generated content.

After Howard Abrams’s How I play TTRPGs in Emacs presentation at EmacsConf 2023 I’m exploring switching to his emacs-rpgdm package. I’ve done some preliminary work, but need to prioritize exploring that. Why? Because his solution for random tables is a bit more robust (though I know I have some edge-cases that are not implemented).

Jump to PDFs or URLs

I have a general text editor function where I can add a line of “metadata” to my Org-Mode document. Namely the path to a resource and it’s label. In this way, my campaign status document has always available links to Errant 📖 ’s rules book and Game Master (GM 📖) screen, a link to the PDF 📖 I’m running, and a link to my Errant Encumbrance, Speed, and Movement Dice Calculator.

This functionality is part of my Emacs project package.

Overview of Characters

I am keeping notes about each PC and NPC ; some more detailed than others. Along with the notes, each character has the following metadata:

  • Name
  • Pronouns
  • Background
  • Demeanor

I can then run a command to quickly show me a tabular view of all characters and those four fields of metadata. What this means is that I can easily transform how I’m looking at the document; from a “book” type format in which I’m looking at long-form notes into a tabular overview of information.

Conclusion

Prior to running online games I never used a computer at my game table. But since adopting Emacs and running sessions online I’ve come around to having a computer behind my GM screen.

Now my “note-taking” page is a high utility multi-functional multi-modal document; an anchor for moving between the myriad of information forms that I reference. But I don’t sit behind the computer. I prefer to stand while running games; when I need the information or want a break from standing I sit down and then reference my computer.

-1:-- Test Driving a Campaign Status Document (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 10, 2023 01:38 AM

Jeremy Friesen: Mentoring VS-Coders as an Emacsian

Metadata

Event URL
https://emacsconf.org/2023/talks/mentor/
Audience
EmacsConf 2023
Presentation Date and Time
at 2:35 pm Eastern Daylight Time (EDT 📖)

Background

Since I have mentored over 40 software developers; many that were in the process of changing careers into software development. I’ve also managed a couple of small software development teams.

Framing Approaches

Remember, we all don’t know what we don’t know.

Be curious while mentoring.

  • Ask questions.
  • Be visible.
  • Pair to share.

What Are You Looking to Learn?

When I start mentoring folks, I ask some form of the following question:

What have you been wanting to learn more of, get better at, and improve on?

Then I ask that they tell me more.

When we later meet, I like to use coaching questions:

  • What’s going well?
  • Where are you getting stuck?
  • If you could change one thing, what would it be?

Make the Work Visible

Like many people, I shifted to remote work in . I have noticed higher collaboration in remote work…when folks make their work visible.

I host office hours.

I try to attend other people’s office hours.

I’ll open up a Slack Huddle and code by myself; but let folks know they can hop in.

Hop In and Be Curious

I pay attention to other huddles that start; if they are still going after 45 minutes or so, I’ll hop in to say hello. It’s even odds that they are moving through the problem or are stuck.

By hopping into that Slack Huddle, I’m helping a common problem: How to know you’re stuck … and when do you ask for help?

The earlier you ask, the quicker the course correction; but too early and perhaps you don’t internalize the lessons of the struggle.

The later you ask, the more you’ll hopefully understand of the sticky problem; however you may have been needlessly added stress and fatigue because others could’ve helped.

Pairing is for Sharing

I tend to let others drive in pairing sessions. They’re typing and working to resolve the problem; and I’m there giving guidance. I’m also spending my time observing how they interact with their text editor. “In the moment,” I limit my advice to one concept; saying something like: control + a will take you to the beginning of the line.

I’ll gently mention that one thing once during our session.

And assuming we have a regular mentoring session, I’ll make sure to ask how they’re feeling about using their tools.

I’d love to get to the point where they ask me: “You saw me using my editor, what’s some things you think I could/should practice/learn?” I’m working on that.

Editor Functions

While pairing, I like to pay attention to how folks handle the following:

  • Where do they want to go?
  • How do they get there?
  • Here they are, now what?
  • How do they summarize?

I know what I can do with Emacs 📖 ; and I assume that Visual Studio Code (VS Code 📖) can do something similar. It’s a matter of helping the mentees find those packages/plugins.

Where Do They Want to Go?

Search within a project
The subtle improvement of Orderless is under-valued
Search across projects
Cross-repository search is simple in Emacs …and I’ve never seen someone do so in VS Code .
Find file names
Many folks use a directory tree to navigate; but I favor fuzzy text. I can get there quickly (see consult-projectile).

How Do They Get There?

Navigation through Language Server Protocol (LSP 📖)
I bind M-. to this; it hurts to watch folks use the mouse to do this simple jump.
File finders
consult-projectile is an amazing multi-function finder.
Jump between definition and test
I bind s-. a sibling of jump to definition; I want more folks to use the Ruby 📖 / RSpec 📖 plugin in VS Code .

Here They Am, Now What?

Word-completion
dabbrev, templates, hippie expand, and completion-at-point; it hurts to watch folks type
Auto-formatting
Tree Sitter 📖 ; I haven’t seen folks install VS Code packages for auto-format.
Multi-Cursor / IEdit
It took a long-time to fully explore iedit; but the practice I did transformed my approach.
Inline searching
My beloved Textmate 📖 introduced me to line filtering; to this day, I use that to understand the neighborhood of text around the text I’m searching.

How Do They Summarize?

I’ve seen a lot of boot camp graduates trained to write commit messages by going to the command line and typing git commit -m. They then use their command line to type out their message.

In my experience, commit messages written on the command line tend to be terse and missing useful context related to the commit. By shifting folks to use their text editor, we’re encouraging an interface where they can write more expressive messages.

Teach them about $GIT_EDITOR and $EDITOR; they’re making commits from the command line. At least help them learn to use their editor.

General Strategies

My goal is to encourage folks to use their editor for writing. To think about owning that tool.

Commit to One Item of Learning Each Week

There’s an absurd abundance of short-cuts, automation, and approaches to solving problems. Don’t overwhelm folks.

As folks are solving problems that “earn” their income, nudge them to practice one thing. That one thing is easier to slot into their everyday work.

Practice within Your Knowledge Domain

Any sufficiently advanced hobby is indistinguishable from work.

I also encourage folks to practice working on problems within a comfortable-to-them domain of knowledge.

I play table top Role Playing Games (RPGs 📖) ; and use that domain as the material for practicing coding. In years past, I’d write Ruby code for dice rollers, note takers, random table lookups. Now I’m doing more of that in Emacs .

Note Taking

Pay attention to how folks create a fleeting note. Some will be in their text editor and anguish about where to create a new file. Others will have a notes application running and write something there.

Help folks think about their note writing habits. Ask about it. Listen to their story and needs.

Help Them Navigate the Proprietary Software Tar Pits

One person was a dedicated Evernote user who has watched their digital contents evaporate. They are approaching their needs very differently than the person that wants their mobile and laptop to have synchronized data so they can have on the go notes that update.

My ideal state is for folks to use their text editor for note taking. Similar to commit messages. Through the language and driver of “getting better at using their text editor.” No need to learn different short-cuts or lament having one tool available in one ecosystem and not the other.

Help Show the Joy of Holistic Computering

Put another way, many folks have ceded different aspects of their digital life to specific and isolated applications. Less prevalent is the holistic thinking of a generalized “computering” environment. Folks need help learning what they don’t know, so they can choose how best to proceed for their lived experiences.

Playing is for Staying

I think one of the reasons I’ve remained a software developer is because I approach all of this as play and story-telling.

Some by-products of that play happens to be shipped features and documentation.

Yet I don’t tell folks to use Emacs ; instead I’m doing my best to show a myriad of reasons for why folks should consider Emacs .

Conclusion

Ask questions of those you’re mentoring; namely how they are looking to improve. Know and show what your editor can do, so that you can make visible learning opportunities for those you mentor.

There’s a mid-level engineer on my team and we both play the “Hey, I learned something new, can I show you?” game. Most important…we play with our editors.

A secondary goal is showing the malleablity of Emacs ; how easy it is to extend.

And obviously there’s so much more than what I’ve highlighted. But…that’s Emacs .

-1:-- Mentoring VS-Coders as an Emacsian (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 03, 2023 09:42 PM

Jeremy Friesen: Emacs Turbo-Charges My Writing

Metadata

Event URL
https://emacsconf.org/2023/talks/writing/
Audience
EmacsConf 2023.
Presentation Date and Time
at 1:00 pm Eastern Daylight Time (EDT 📖) .

Introduction

I’ve been employed as a software developer since . I’ve been writing for my personal blog since . In I switched to Emacs 📖 ; Previously Atom (Atom 📖) , Sublime Text 📖 , and Textmate 📖 ; with numerous failed forays into Vim 📖 and a turn away in disgust of Visual Studio Code (VS Code 📖) and it’s belittling treatment of real estate for writing commit messages.

Curious about my public writing habits, I added a Post Statistics page to my personal site. Prior to Emacs , I had about a 95 words per day public output. After switching to Emacs I’m now averaging 340 words per day.

Delta, Delta, Delta

There are many reasons for this change:

  • A pandemic removed my commute of 2 hours per day.
  • A pandemic shifted my habits towards more alone/contemplative time.
  • My children graduated secondary school and moved out.
  • My wife’s business became more self-sustaining.
  • I expanded the scope of my blog; from gaming towards everything that I want to publicly write about.
  • I changed employers; to one that encouraged public knowledge sharing.

All of this with Emacs as my supporting technology.

I have extended my Emacs to reduce barriers to capturing, synthesizing, referencing, and publishing my thinking and knowledge sharing.

And I’m always looking for refinements.

Well, How Did I Get Here?

I switched from WordPress 📖 to Jekyll 📖 then to Hugo 📖 . When I switched from WordPress to Jekyll I fully adopted Markdown.

During that switch I was learning Emacs ; and hesitant to learn Org-Mode 📖 . It seemed like too much to introduce.

I was still keeping my public writing separate from my personal/private writing.

Keep Em’ Separated

I didn’t realize how much that separation created mental friction in my writing:

  • Where should I write this?
  • If I want to switch from private to public, what might I need to sanitize?

Further complicating things, private stuff used one markup format; public stuff had different markup.

I wrote down a data flow diagram to help think through how all local writing and links could move to a world-viewable state without private information also bleeding out.

Put another way, I have done the “pre-thinking” about where things should go. Now I file them and can easily refile them.

Domains for Notes

For this presentation, there are four notable domains:

blog-post
I’m going to be publishing this.
epigraph
Meaningful quotes that I’ve gathered in my writing.
glossary
These are a term or concept.
melange
I don’t know where it goes, so it goes here.

It is trivial to move notes between domains.

Exploratory Exhumation and Exhilarating Exposition

I began exploring Org-Mode , via Org-Roam 📖 , while also reading How to Take Smart Notes by Sönke Ahrens.

I experimented with some of those Zettelkasten 📖 approaches and began a journey of crafting and continuously refactoring my Personal Knowledge Management (PKM 📖) process. What comes next is a demonstration.

In that time I’ve switched from the amazing Org-Roam (by Jehtro Kuan) to the simplified (see less dependencies) Denote 📖 package (by Protesilaos Stavrou (Prot 📖) ).

Demo Time

A stark difference for my preferred environment; be it writing or coding is that I don’t use:

  • tabs
  • file/directory tree navigator
  • multiple monitors

I aim to have one visible buffer; and split as needed. In other words, my hope is to minimize focal distractions.

Let’s get started.

Create a Note

I have mapped the H-d c prefix to the mnemonic “Denote Create…” something. Through the which key 📖 package, I then show the functions for each of the “domains” I might write about.

Let’s start a blog post. Sidenote My blog makes extensive use of sidenotes and marginalia.

Dabbrev and Hippie Expand

After watching Jay Dixit’s Emacs for Writers presentation, I adopted Dabbrev for auto-correction. I also make use of Hippie expand for word completion.

While pairing with those that use VS Code , I always wonder what tools they could use to match the simple functionality of the Hippie Expand and Dabbrev packages.

These are now so foundational, I don’t think about them. They just make writing easier.

Let’s make a few links:

  • Let’s link to a note that does not have a “public” representation.

  • Let’s link to a note that references a “public” resource (e.g. has a URL 📖 )

    • These will export as CITE and/or A tags.
  • Let’s convert a note link from a “title” to an abbreviation.

    • Abbreviations export as ABBR tags.
  • I also have a date: type link

    • Exports as TIME tag.
  • Let’s insert an epigraph.

I want my notes to move towards the most relevant note, but be discoverable based on how my brain filed things away. With back-linking, I’m regularly leaving breadcrumbs to help find my way to the relevant information.

Moving Through the Thoughts

Put another way, links and back-links provide another means of discovery within my 3400 or so notes.

As are hand-crafted indices; file searches; file names; and tags.

Clocking

Sometimes, I’ll be writing and want to reference code (or prior blog posts). I’ll use Org-Mode ’s capture to clock functionality to help quickly gather references. There are other ways to approach this, but it’s what I have.

Macros

I use Daniel Mendler’s Tempel package. For Org-Mode , I have a few templates that inject macro markup.

When I cite non-glossary entries, I use H-m c to insert a cite macro, and prompt for existing “cite” references in the document. Or C-u H-m c to prompt for all sources in my PKM .

I have cite, keyboard, idiomatic, and mechanic that are all part of the H-m prefix group. These macros evaluate differently for Hugo compared to LaTeX 📖 .

These macros help me write more semantic HTML .

Blocks

Org-Mode ’s default org-insert-structure-block is great, but I want further prompts. So I wrote a function that will take typing ``` as the first characters of a line and prompt for the type of block.

Let’s look at the “Blockquote” option. It prompts me for author, cite, and cite url. It builds from the org-insert-structure-block but prompts me to consistently fill out optional information.

These prompts also help me remember the syntax of writing these custom blocks.

Abstract

As I wrap up my writing, I include an abstract/summary. This is my chance for synthesis and to help possible future readers determine if this article is relevant to their interests.

It’s also what I use for the page’s meta description and eventually text for posting the link to Mastodon 📖 .

When I first started blogging, I wanted to “publish” and didn’t take the time to summarize. Now, I consider it useful to “pitch” the document I wrote.

Export

With all of that, I export the document to my blog (https://takeonrules.com).

Let’s look at the Markdown.

And the home page: http://localhost:1313

Note: I have a Hugo server running on this port.

Conclusion

When I started learning Emacs , I quickly shifted to “Vanilla” Emacs and just started writing.

As I wrote, when I needed to do something that I’d previously done in a text editor, I’d find and experiment with different packages.

I continue with that mindset. As I write, I’m attending to what I’m doing. And eventually I realize “if I were to just write a function that does this one thing…I’d have a smoother writing experience.”

The goal is to minimize focal distractions, while also helping me create breadcrumbs that lead back to my writing and thoughts.

One of those “functions” I’d like to write is extending my abbr: export to work with LaTeX ; it’s halfway there I just need to prioritize and spend an evening of playing with Emacs .

-1:-- Emacs Turbo-Charges My Writing (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 03, 2023 04:43 PM

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

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. Sidenote Especially with the amazing consult-projectile package.

However in a new project Hyku 📖 we’ve started using Git submodules. Sidenote 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. Sidenote 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

update:

, 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. Sidenote 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. Sidenote 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))))

Conclusion

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

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 📖 . Sidenote 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. Sidenote 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. Sidenote https://github.com/jeremyf/vibrant-ink-ruby I remember practicing Vim 📖 but the paradigm did not stick. Sidenote 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. Sidenote 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. Sidenote 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. Sidenote 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.

Conclusion

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

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. Sidenote 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 . Sidenote 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

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."
  (interactive)
  (let* ((f (or filename (buffer-file-name)))
         (relative-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. Sidenote 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.

Preliminaries

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)

Rancher

Copy the contents of Rancher’s cluster’s KubeConfig into a local file. Sidenote 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

, 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

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

Functionality

, 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. Sidenote I do need to consider randomizing it’s level; higher level henchmen are available at larger urban centers.

(random-table/register
 :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:

(random-table/register
 :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.
(random-table/register
 :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)))

(random-table/register
 :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. Sidenote 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)
            (list
              (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.

Conclusion

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

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

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.

Code

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)
       ,docstring
       (if current-prefix-arg
         (read-number (format "Roll %s for %s: " ,label (random-table-name table)))
         ,@body))))

(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:

(random-table/register
   :name "Random Attribute"
   :data '("Strength"
           "Constitution"
           "Dexterity"
           "Intelligence"
           "Wisdom"
           "Charisma"))

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.

Conclusion

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

Any sufficiently advanced hobby is indistinguishable from work.
Jeremy Friesen

Introduction

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)
  (completing-read
   (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. Sidenote 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))))
    (cond
      ((= 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).

Conclusion

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

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!