Emacs@Habrahabr: Ричард Столлман, автор GCC и Emacs



Сложно найти человека, про которого ходит больше легенд, чем Ричард Столлман (RMS). Наверное, по количеству невероятных историй и анекдотов с ним может сравниться только Джефф Дин.

Причём многие невероятные истории о Столлмане — правда. Например, что он запрашивал веб-страницы wget-демоном по почте (для безопасности) и отказался от использования мобильных телефонов, которые называет портативными устройствами слежения. На своём переделанном ноутбуке Thinkpad x200 он в основном работает в консоли и старается не подключаться к веб-сайтам с собственной машины.

В 71 год Столлман борется с раком (успешно) и пострадал от травли за свои спорные высказывания и действия. Но его достижений хватит на десятки или сотни великих жизней. Он является автором компилятора GCC и текстового редактора Emacs, а также основателем проекта GNU, дополнением к которому стало ядро Linux, в результате чего появилась полноценная ОС.

Гений Столлмана опередил время. Только в 21 веке — с ползучим наступлением цифрового тоталитаризма, всеобщей слежки в духе «1984» и алгоритмизацией всех аспектов жизни — люди постепенно начинают понимать, что Ричард Столлман был прав.
Читать дальше →
-1:-- Ричард Столлман, автор GCC и Emacs (Post alizar (RUVDS.com))--L0--C0--November 18, 2024 09:01 AM

Emacs@Habrahabr: GNU Emacs: вопроизводимая сборка всё ближе

Одна из проблем GNU Emacs заключается в сложности создания воспроизводимой сборки. Разумеется, вы всегда можете сделать себе контейнер, и запускать Emacs с помощью Docker, Podman или даже Kubernetes... Но я сейчас не о таких сложных случаях.

Итак, суть проблемы: если создать файл init.el и отладить его, то через 2-3 месяца запуск с ним Emacs на другом компьютере с большой вероятностью приведёт к ошибкам установки или несовместимости пакетов.

В этой статье рассматривается использование возможностей встроенного пакета package-vc-install.el для решения описанной проблемы.

(goto-address-at-point)
-1:-- GNU Emacs: вопроизводимая сборка всё ближе (Post dunmaksim)--L0--C0--October 01, 2024 02:35 PM

Jeremy Friesen: Exporting Org Mode Elfeed Links

I make extensive use of Org-Mode 📖 ’s linking functionality. Sidenote See External Links (The Org Manual). , I quickly extended the functionality of elfeed 📖 links to include an :export option.

Use Case

While reading my RSS 📖 in elfeed I might want to link to that entry in another document. I can save a link to the current feed entry by invoking org-store-link. Sidenote See Handling Links: org-store-link (The Org Manual).

Then in the other document, I can invoke org-insert-link. Sidenote See Handling Links: org-insert-link (The Org Manual). This will insert a link with a elfeed protocol and encode either an entry or a filter. In the case I’m looking to handle, I want to export an entry not as an elfeed link but instead to resolve that link to its origin.

Below is the code that implements by use case:

(org-link-set-parameters "elfeed"
  :follow #'elfeed-link-open
  :store #'elfeed-link-store-link
  :export #'elfeed-link-export-link)

(defun elfeed-link-export-link (link desc format _protocol)
  "Export `org-mode' `elfeed' LINK with DESC for FORMAT."
  (if (string-match "\\([^#]+\\)#\\(.+\\)" link)
    (if-let* ((entry
                (elfeed-db-get-entry
                  (cons (match-string 1 link)
                    (match-string 2 link))))
               (url
                 (elfeed-entry-link entry))
               (title
                 (elfeed-entry-title entry)))
      (pcase format
        ('html (format "<a href=\"%s\">%s</a>" url desc))
        ('md (format "[%s](%s)" desc url))
        ('latex (format "\\href{%s}{%s}" url desc))
        ('texinfo (format "@uref{%s,%s}" url desc))
        (_ (format "%s (%s)" desc url)))
      (format "%s (%s)" desc url))
    (format "%s (%s)" desc link)))

I looked to the existing elfeed-link package and found the useful regular expression for parsing the link, and it was a quick matter of writing up the format pcase logic.

I wrote Hacking Org-Mode Export for Footnotes as Sidenotes in which I over-wrote the default footnote export behavior.

I have also implemented a few different formats:

abbr
for linking to my personal glossary and rendering not as a link but instead an ABBR tag.
date
to export a TIME tag, and also allow me to (eventually) create a link back function for dates.
epigraph
for rendering a blockquote of an epigraph that I find meaningful for the blog post.

In each of these links the goal is to create extensions to the default semantics of Org-Mode , such that I can have semantic markup in my export.

-1:-- Exporting Org Mode Elfeed Links (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 11, 2024 01:22 PM

Jeremy Friesen: Releasing v0.8.0 of random-table.el

I added a new feature to my Random Tables package: the ability to prompt for a label and pick options. These are two separate features but related by their use case.

First let’s look over the Use Case, then at the Random Tables documentation.

Use Case

In Solo Gaming, one concept is the idea of asking an open-ended question, then consulting an oracle. Consulting an oracle often involves rolling on one or more tables based on the nature of the question.

In The One Ring 📖Strider Mode 📖 has a Lore Table that has three categories:

  • Action
  • Aspect
  • Focus

Examples of questions and their choices are as follows:

  • “What is the nature of these woods?” (Aspect)
  • “What mission does my patron have for me?” (Action,

Aspect, Focus)

In the above example, I ask an open-ended question and then consult one or more additional tables.

Documentation

Below is an example of a table that will:

  • Prompt for a :label.
  • Prompt to pick one or more :options and from the chose options parse the selected option(s).
(random-table/register :name "The One Ring > Lore"
  :data '("{The One Ring > Lore @label}{The One Ring > Lore @options}")
  :label '(read-string "Question: ")
  :options '(("Action" . "- Action :: {The One Ring > Lore Table > Action}")
              ("Aspect" . "- Aspect :: {The One Ring > Lore Table > Aspect}")
              ("Feature" . "- Feature :: {The One Ring > Lore Table > Feature}")
              ("Focus" . "- Focus :: {The One Ring > Lore Table > Focus}")))

The syntax is {<TABLE_NAME> @label} to apply the :label function.

For parsing :options you can use {<TABLE_NAME> @options} for multi-select and {<TABLE_NAME> @option} for singular select. The parsed options are prefixed with \n\t.

Let’s say we roll on the “The One Ring > Lore” table ask the following question: “What are the orcs doing?”. We then choose the Action option which will roll on the “The One Ring > Lore Table > Action”. Which would output something similar to the following:

What are the orcs doing?
- Action :: Chase

Were we to select Action and Focus we’d get something similar to the following output:

What are the orcs doing?
- Action :: Chase
- Focus :: Luck

Implementation Details

In Introducing Extensibility with a Macro, a List, and a Reducer, I defined an interface for functions to parse the tabular strings. And then using reduce I iterated through those functions with the original string; rolling each result.

Using the random-table/create-text-replacer-function macro I created two new functions:

 (random-table/create-text-replacer-function
  "Conditionally replace TEXT with table's label function.

 The label function is called via `apply'."
  :name random-table/text-replacer-function/label
  :regexp "{\s*\\([^@]+\\)@label\s*}"
  :replacer (lambda (matching-text table-name)
             (if-let* ((table (random-table/fetch
                               (string-trim table-name) :allow_nil t))
                       (prompt (random-table-label table)))
                 (apply prompt)
               matching-text)))

 (random-table/create-text-replacer-function
"Conditionally replace TEXT with table options."
:name random-table/text-replacer-function/options
:regexp "{\s*\\([^@]+\\)@option\\(s\\)?\s*}"
:replacer (lambda (matching-text table-name &optional plural)
             (if-let* ((table
                        (random-table/fetch
                         (string-trim table-name) :allow_nil t))
                       (options
                        (random-table-options table)))
                 (let ((joiner "\n\t")
                       (picked
                        (if plural
                            (completing-read-multiple "Options: " options nil t)
                          (list (completing-read "Options: " options nil t)))))
                   (concat
                    joiner
                    (s-join joiner
                            (mapcar (lambda (el)
                                      (alist-get el options nil nil #'string=))
                                      picked))))
               matching-text)))

I then added these functions to the random-table/text-replacer-functions and presto, I had new functionality.

Next Steps

As I’m leveraging a struct for the table definition, I’m considering adding an :extensions field and then a general extension reducer, in which each extension would have it’s on parser function.

What I have works for now, and I’m pleased that it took about 30 minutes to identify the use-case, a syntax, and extend it.

-1:-- Releasing v0.8.0 of random-table.el (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 04, 2024 12:33 AM

Jeremy Friesen: Using the Link Hint Package to Extend Eldoc Functionality

Over the last few months, I’ve been writing Go Lang 📖 code. I use the eglot package for Language Server interaction.

What I like about eglot is that it wires into built-in Emacs 📖 packages. For “popup” documentation, eglot integrates with the eldoc 📖 package. In my setup the results from the gopls 📖 language server would often show what looked like links but were not clickable.

When I hovered with my mouse, I would see the URL to the documentation. But there was no bound action to text.

A screenshot of a portion of Emacs application with annotations for where the cursor is at, the mouse is hovering, and what I see on hover.

As a long-time user of Link Hint, I figured I would explore adding a new handler.

First, I started exploring some of the existing handlers. Many of them looked for propterties on the rendered text. I inspected the text properties of what looked like a link and saw that the help-echo property contained the URL.

From that I followed the documentation and settled on the following configuration to expose eldoc links to the link-hint commands:

;; Allow for opening the URLs provided in eldoc.  This is rough and
;; minimal error handling.
(link-hint-define-type 'eldoc-url
  :next #'link-hint--next-eldoc-url
  :at-point-p #'link-hint--eldoc-url-at-point-p
  :open #'browse-url
  :copy #'kill-new)

(defun link-hint--next-eldoc-url (bound)
  "Get position of next `face' at or after BOUND."
  ;; While we're interested in the 'help-echo value, we need to see if
  ;; we can't solely work from that.  Instead we need to check if we
  ;; have a link face.
  (link-hint--next-property-with-value 'face 'markdown-link-face bound))

(defun link-hint--eldoc-url-at-point-p ()
  "Return the name of the eldoc link at the point or nil."
  ;; Mirroring `link-hint--next-eldoc-url' logic, when we have a link
  ;; face look to the help-echo for the URL.
  (when (eq (get-text-property (point) 'face)
          'markdown-link-face)
    (get-text-property (point) 'help-echo)))

(push 'link-hint-eldoc-url link-hint-types)

Now, when I invoke M-x display-local-help, I can then invoke M-x link-hint-open-link which will include the inline documentation text as a candidate for opening a link.

update

My initial implementation was too greedy; namely it was finding other help-echo properties and clobbering those linking abilities. This solution is more specific, but still might benefit from a URL verification on that help-echo; but that is a future exercise. What I have is adequate.

-1:-- Using the Link Hint Package to Extend Eldoc Functionality (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--June 21, 2024 08:12 PM

Jeremy Friesen: Adding a Consult Function for Visualizing Xref

Over the last month, I’ve started working in a rather large code-base. And I’m using the eglot package to help navigate to and from code definitions; under the hood eglot connects into the xref package, a tool for navigating to programatic symbols.

The xref package exposes several functions, creating a bit of a breadcrumb trail:

xref-find-definitions
Find the provided symbol’s definition; usually this is the symbol at point.
xref-go-back
Go back to the previously found symbol.
xref-go-forward
Go forward to the next found symbol; available when you went back.

And on some of my code expeditions, I jump to a definition, then another definition and so forth. It can be easy to lose my place; even as xref tracks where I’ve been.

But to my knowledge, the breadcrumbs aren’t visible…until I wired up a Consult function.

(defvar consult--xref-history nil
  "History for the `consult-recent-xref' results.")

(defun consult-recent-xref (&optional markers)
  "Jump to a marker in MARKERS list (defaults to `xref--history'.

The command supports preview of the currently selected marker position.
The symbol at point is added to the future history."
  (interactive)
  (consult--read
    (consult--global-mark-candidates
      (or markers (flatten-list xref--history)))
    :prompt "Go to Xref: "
    :annotate (consult--line-prefix)
    :category 'consult-location
    :sort nil
    :require-match t
    :lookup #'consult--lookup-location
    :history '(:input consult--xref-history)
    :add-history (thing-at-point 'symbol)
    :state (consult--jump-state)))

This function behaves very much like Consult’s consult-mark function; showing a preview of each of those xref locations.

-1:-- Adding a Consult Function for Visualizing Xref (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--June 09, 2024 02:03 AM

Jeremy Friesen: Adding a Project Search Option to Ignore Swagger Generated Files

As part of my new job, I’m working in Go Lang (Go 📖) and writing Swagger 2.0 specifications. We use Go Swagger 📖 to generate code from the specification. This results in lots of files that have the preamble: “// Code generated”.

When searching the contents of a project, I often want to exclude those files.

, I extended the amazing Emacs 📖 package for Ripgrep 📖 ; aptly named rg.el.

I created jf/rg-get-not-generated-file-names; it would find all of the files that:

  • did not have the phrase //Code generated
  • were not in the ./vendor directory
(defun jf/rg-get-not-generated-file-names ()
  "Create a custom type glob for files that were not generated."
  (format "{%s}"
    (string-trim
      (shell-command-to-string
        (concat
          "rg -e \"^// Code generated .*DO NOT EDIT\\.$\" . "
          "--files-without-match --glob=\\!vendor | tr '\\n' '\\,' "
          " | sed 's|,$||' | sed 's|\\./||g'" )))))

The function transforms those files into a suitable glob format, which I can then specify as the :files value for the rg-define-search macro.

(rg-define-search rg-project-not-generated
  "Search only within files that are not generated."
  :files (jf/rg-get-not-generated-file-names)
  :dir project
  :menu ("Search" "n" "Not Generated"))

And just like that, I have an option to search within the non-generated files of my project; that is files that a human has more likely crafted.

Alternate Approach

The above functions work within the constraint of rg.el. When I’m on the command line, I can do the following:

rg -e "^// Code generated .*DO NOT EDIT\.$" . \
   --files-without-match --glob="\!vendor" | \
    xargs rg MY_PATTERN

My initial approach with rg.el was to attempt this very thing, but it would’ve involved a lot more work.

-1:-- Adding a Project Search Option to Ignore Swagger Generated Files (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 28, 2024 10:33 PM

Jeremy Friesen: Adding an Emacs Command to Summarize Headlines with Chosen Tags

I’ve been playing a solo game of Ironsworn: Starforged 📖 . Follow session reports if you are interested.

I’m using Emacs 📖 for tracking that campaign, and exporting those sessions to blog posts. While playing I often find myself needing to review information.

I set about writing a function to help me review those notes by rendering a summary of the headlines with matching tags.

I also set about writing this function to be generally useful.

Organization of My Notes

The following represents most of the hierarchy of my notes; with the words in parentheses indicating the tags.

  • Character (#characters)
    • Attributes (#attributes)
    • Assets (#assets)
    • Vows (#vows)
    • Legacy Tracks (#tracks #legacy)
  • Expedition Tracks (#tracks #expeditions)
  • Clocks (#clocks)
  • Connections (#connections)
  • Factions (#factions)
  • Spaceships (#spaceships)
  • Sectors (#sectors)
    • Sector
      • Systems (#systems)
        • Settlements (#settlements)
  • Sessions (#sessions)

Below each of those headlines are additional detailed headlines. Important in this is that all headlines inherit the tags of their ancestor headlines.

Within each headline, I might write properties; the character’s momentum attribute for example. Or the progress track for a vow.

There’s quite a lot of information that I might need to cross-index while playing.

The Functions of Summarization

I ended up writing two functions:

jf/org-mode/buffer-headline-tags
Create a list of all non-file level tags.
jf/org-mode/summarize-tags
Prompt for the tag or tags to summarize then render a summary of headlines that match those tags.

The summary is itself an Org-Mode 📖 document that shows each headline and the relevant properties; suitable for quick scanning.

jf/org-mode/buffer-headline-tags

The jf/org-mode/buffer-headline-tags function:

(defun jf/org-mode/buffer-headline-tags ()
    "Return a list of `org-mode' tags excluding filetags.

  In the present implementation, I'm relying on `denote'
  conventions.  However, by creating a function I'm hiding the
  implementation details on how I get that."

    ;; This is here to indicate the dependency
    (require 'denote)
    (let* ((all-tags
             (org-get-buffer-tags))
            (file-level-tags
              (denote-extract-keywords-from-path (buffer-file-name))))
      ;; Given that I want inherited tags and the filetags are
      ;; considered to be on all headlines, I want to remove those tags.
      (cl-reduce (lambda (mem el)
                   (if (member
                         (substring-no-properties (car el))
                         file-level-tags)
                     mem
                     (add-to-list 'mem el)))
        all-tags :initial-value '())))

jf/org-mode/summarize-tags

And the jf/org-mode/summarize-tags

(defun jf/org-mode/summarize-tags (&optional tags)
    "Create `org-mode' buffer that summarizes the headlines for TAGS.

This reducing function \"promotes\" the property drawer elements
to list elements while providing the same functionality of an
`org-mode' buffer.

Some of this could be accomplished with column/table declarations
but the headlines and content might not fit so well in the
buffer."
    (interactive (list
                   (completing-read-multiple
                     "Tags: "
                     (jf/org-mode/buffer-headline-tags) nil t)))

    (require 's)
    ;; With the given tags map the headlines and their properties.
    (let* ((prop-names-to-skip
             ;; This is a list of headline properties that I really
             ;; don't want to report.  I suspect some may be buffer
             ;; specific.  But for now, this should be adequate.
             ;;
             ;; Perhaps in later iterations we'll prompt for additional
             ;; ones to ignore.
             '("ID" "ALLTAGS" "FILE" "PRIORITY" "ITEM" "TIMESTAMP"
                "TIMESTAMP_IA" "CATEGORY" "TAGS"
                "BLOCKED" "TODO" "CLOSED"))
            (text-chunks
              ;; In using `org-map-entries' I can access inherited tags,
              ;; which I find structurally useful
              (org-map-entries
                ;; Yes this could be its own function but for now, we'll
                ;; leave it at that.
                (lambda ()
                  (let* ((h (org-element-at-point))
                          ;; Rebuild a terse header: depth, todo, title
                          ;; only
                          (header-text
                            (format
                              "%s%s %s\n"
                              (s-repeat
                                (org-element-property :level h) "*")
                              (if-let ((todo
                                         (org-element-property
                                           :todo-keyword h)))
                                (format " %s" todo)
                                "")
                              (org-element-property :title h)))

                          ;; Only select relevant properties, converting
                          ;; those properties into a list of strings.
                          (properties-text
                            (cl-reduce
                              (lambda (mem prop-value)
                                (if (member (car prop-value)
                                      prop-names-to-skip)
                                  mem
                                  (add-to-list 'mem
                                    (format "- %s :: %s"
                                      (car prop-value)
                                      (cdr prop-value))
                                    t)))
                              (org-entry-properties h)
                              :initial-value nil)))

                    ;; If we have properties we want to render, we'll
                    ;; have one format.
                    (if properties-text
                      (format "%s\n%s\n" header-text
                        (s-join "\n" properties-text))
                      header-text)))
                (s-join "|" tags)
                'file 'comment))
            ;; Let's have only one of these
            (buffer-name "*Org Mode Tag Summary*")
            (display-buffer-mark-dedicated t))

      ;; We've run this command again, so let's destroy what we had
      ;; and start anew.
      (when (get-buffer buffer-name) (kill-buffer buffer-name))
      (get-buffer-create buffer-name)
      (with-current-buffer buffer-name
        ;; Minimize the chatter of the mode-line
        (let ((mode-line
                (concat
                  (propertize (format "%s Tags: #%s"
                                ;; Show a lock icon
                                (char-to-string #xE0A2)
                                (s-join " #" tags))
                    'face 'mode-line-buffer-id)
                  "  "
                  (propertize
                    "C-c C-k to exit"
                    'face 'jf/mode-line-format/face-shadow))))
          ;; This came from `org-mode' so let's continue to keep it that
          ;; way.
          (org-mode)
          (insert (s-join "\n" text-chunks))
          ;; Let's not have the illusion that we're allowing ourselves
          ;; to edit this text
          (read-only-mode)
          (pop-to-buffer buffer-name
            `((display-buffer-in-side-window)
               (side . right)
               (window-width 72)
               (window-parameters
                 (tab-line-format . none)
                 (mode-line-format . ,mode-line)
                 (no-delete-other-windows . t))))))))

Conclusion

While I envisioned these functions to help facilitate Role Playing Game (RPG 📖) sessions, I know I’ll get use out of jf/org-mode/summarize-tags for some of my non-gaming notes.

Now, maybe instead of writing tools to help facilitate playing a game, I should sit down and play a game.

-1:-- Adding an Emacs Command to Summarize Headlines with Chosen Tags (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 19, 2024 07:50 PM

Jeremy Friesen: A Quiet Morning of Practice to Address an Observed Personal Computering Workflow Snag

, afternoon I was pairing with a colleague. I was driving as she was helping navigate. Both of us were unfamiliar with the code-base, but she was more familiar with the technical domain. Sidenote She had done a bit of Angular, but quite a bit of React. Meanwhile, I was at hour 6 of my “break fixing” Angular journey.

Taking Private Time to Think

There were times where she would tell me “I need to orient to this code-base; I’m going to disconnect and re-join in 10 minutes.” In the moment, I thought that was a unique response, but I thought about it more: she needed time to use her tools to “load” a better working understanding of the project.

Awesome, I thought. I know the all too real sensation of “lack of control” when pairing. I learn code by moving through it. Sidenote See ’s post Modifying Emacs link-hint Package Open URL Behavior for Local Repositories for some glimpses of my process.

My Own Exploration

During one of those moments when she went to do her own research, I started down an exploratory thread that required me to have open four different files:

  • Component A
  • Component A’s button sub-component
  • Component B
  • Component B’s button sub-component

Why? Because Component A and Component B had similar functional expectations, but when I clicked the Component A button it would reset data on the web-page. I did not see that behavior when I clicked Component B’s button.

I started reading and refactoring, as I saw how these two components were constructed differently. I had my editor configured such that:

  • Component A in top left
  • Component A’s button in bottom left
  • Component B in top right
  • Component B’s button in bottom right

Most of the times when I’m coding, I have one or maybe two files open at the same time. This four-way split is very infrequent. But here I was using comparisons to help guide a refactor towards similarity.

Return to Pairing

And then my colleague hopped back on and she was ready to try something. In that moment, I realized “I haven’t learned how to save window state for later re-use.”

Not wanting to delay our pairing time, as we were coming up on 5pm on a Friday afternoon, I hid that context and opened a new one. Yet a little gremlin in the back of my head kept nagging me that I might lose that window context.

We continued pairing and got to a point where I had what I think to be enough to proceed on my own. Sidenote A problem I left for Jeremy to deal with.

Listening to that Little Gremlin

But I listened to that little gremlin and made a mental note to practice and learn two things:

  • How to swap the location of windows in Emacs 📖 .
  • How to save the configuration of windows in Emacs .

Swap Location of Windows

I’ve long used the Ace Window 📖 package to quickly jump from one window to another. I have also used Ace Window to flip the location of two windows that are the only windows in a frame.

So I knew there was a way to swap the location of one window with another window when there were more than two windows in the frame.

I explored the documentation a bit more, and found ace-swap-window, which can be invoked within the M-x ace-window command.

This would help me better reconstruct the aforementioned four window arrangement; as I could re-arrange later.

Save Configuration of Windows

The other issue, namely preserving windows state over the weekend required a different approach. I know that I can use M-x window-configuration-to-register, and did in the moment. However, in my experiments this was not as reliable as I’d hoped. Sidenote I got some buffer gone errors as I moved about my business.

As a regular subscriber of Sacha Chua’s blog, I had seen project-butler roll across my feed earlier this week. I did some reading, at it wasn’t quite what I was looking for. But it did mention (and remind me of) the Activities 📖 package.

I did some quick reading, and it looked like what I was after. So I installed it and started exploring.

And it is exactly what I want for that situation and perhaps others. It allows me to take a brief moment before switching contexts to consider bookmarking my current activity.

Conclusion

I’ve done a lot of pairing over the years and have often used pairing as a chance to learn other people’s approaches. I found it inspiring that in the moment of our pairing, my coworker asked and took a few breaks to further investigate. She came back with greater context and we moved quickly to isolating the specific problem.

I also took that time to observe that I had created a situation for myself that I wanted to learn how to better navigate, namely having a context that I wanted to “stash” and re-join our shared context.

I spent time reflecting on both of these moments and took time to practice and reflect on my observations.

-1:-- A Quiet Morning of Practice to Address an Observed Personal Computering Workflow Snag (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 18, 2024 03:40 PM

Jeremy Friesen: Modifying Emacs link-hint Package Open URL Behavior for Local Repositories

I use Org-Mode 📖 as my markdown format of choice. One feature that I use as I work through code is capturing to a current clock. Sidenote See Clocking Work Time (The Org Manual). My Custom Org-Mode Capture Function for Annotating Bad Code walks through some of that functionality; however I’ll go over a refined workflow.

Background

As part of my new role, I’m working on a code base that is new to me. It’s also in a language that I’m familiar with but by no means an expert: Javascript. And the framework chosen is Angular. All told a lot of new concepts.

I have a task at work that requires fixing a bug; and it’s confounding as it’s related to mutating state.

In order to orient, I use the capture to clock concept. In my projects file, I created a headline (e.g. “Working through UI Reset Bug”). Then I start a clock for that headline. And I begin my code exploration.

When I find a bit of code to consider, or that I want to “find again”, I invoke org-capture Sidenote See Capture (The Org Manual). and pick my “Capture to Clock”. Sidenote My “Capture to Clock” template is defined here. This grabs the current selected region and creates a link to the remote repository. I then add some more information about why it’s important and then complete the capture.

Prior to , I would also include a link to the local file. But I removed that. In part because I would often times export that headline as a PDF or Markdown and share with teammates; and they likely didn’t have those files on their machine.

I still wanted to open those code blocks locally in Emacs 📖 over opening the remote document. Which lead me down a bit of exploration.

I have for quite some time used the Link Hint package. I love it. I invoke M-x link-hint-open-link and am presented with several overlays of characters. Each overlay is position above a “link” that Emacs knows how to handle. Sidenote Which is a lot of things.

I type the character combination to select the link I want, and it opens the link in the “correct” application. In the case of https, it defaults to using my browser…until .

I added some advice that will intercept handling any Org-Mode link and test if the link’s URI is a Github URL.

If it is, I test if I have that repository on my local machine. And when I do, I jump to the local file.

Below is that code:

(defun jf/link-hint--apply (advised-function func &rest args)
  "Hijack opening `org-mode' URLs by attempting to open local file.

The ADVISED-FUNCTION is the original `link-hint--apply'.

The FUNC is the dispatched function for handling the link
type (e.g. `link-hint--open-org-link').

The ARGS are the rest of the ARGS passed to the ADVISED-FUNCTION."
  (if (and
       (eq 'link-hint--open-org-link func)
       (eq :open (caddr args)))
      (progn
        (if-let* ((url
                   (car args))
                  (_match
                   (string-match
                    (concat "^https://github.com/"
                            "\\([^/]+\\)/\\([^/]+\\)" ;; org/repo
                            "/[^/]+/[^/]+/" ;; blob/sha
                            "\\([^#]+\\)" ;; path to fil
                            "\\(#L\\([0-9]+\\)\\)?") ;; line number
                    url)))
            ;; Due to my present file structure I have some repositories
            ;; in ~/git/ and others in ~/git/sub-dir
            ;;
            ;; In most every case, the Github org and repo match the
            ;; remote URL.
            (let ((filename-without-org
                   (format "~/git/%s/%s"
                           (match-string 2 url)
                           (match-string 3 url)))
                  (filename-with-org
                   (format "~/git/%s/%s/%s"
                           (match-string 1 url)
                           (match-string 2 url)
                           (match-string 3 url)))
                  (line-number
                   (match-string 5 url)))
              (cond
               ((f-exists? filename-without-org)
                (progn
                  (find-file filename-without-org)
                  (when line-number
                    (goto-char (point-min))
                    (forward-line
                     (1- (string-to-number line-number))))))
               ((f-exists? filename-with-org)
                (progn
                  (find-file filename-with-org)
                  (when line-number
                    (goto-char (point-min))
                    (forward-line
                     (1- (string-to-number line-number))))))
               (t (funcall func args))))

          (funcall func args)))
    (apply advised-function func args)))

(advice-add 'link-hint--apply
            :around #'jf/link-hint--apply)

As always, there’s room for refactor. But this gets the job done.

Conclusion

When exploring a code-base, I’m finding it helpful to annotate my explorations and be able to refer back to those findings.

With this change, I can continue to do that without sharing documents that have extra chatter regarding “local links.”

I used the above process (with local links) to add my working notes to a Github issue. I intended for those notes to be a way-finding tools for other folks who might come along; or for someone who might help guide me in a more fruitful direction.

Post Script

I’ve also used the process of capturing to a clock to create blog posts as well as run-book pages. In fact, for this post I started a clock on the Link Hint Package heading and then used my capture process to grab the code.

-1:-- Modifying Emacs link-hint Package Open URL Behavior for Local Repositories (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 17, 2024 09:52 PM

Jeremy Friesen: Creating an Emacs Helper Function for Incrementing a Game Clock Track

I’ve been playing a solo game of Ironsworn: Starforged 📖 . Thus-far I haven’t published any of the sessions, but I’m looking to do so.

One of the concepts of Ironsworn: Starforged is the track. Each track has ten boxes and each box can have 0 through 4 marks. When a box has 4 marks, it is considered full.

I find 󰪥󰪥󰪡 a much nicer representation than 6 of 40. But 6 of 40 is “easier” to do math against.

What follows is an Emacs function that will increment a track (e.g. 󰪥󰪥󰪡) by a number of ticks (e.g. 3). And it returns 󰪥󰪥󰪥󰪟.

(defun jf/gaming/clock-track-incremement (track ticks)
  "Return a TRACK incremented by a number of TICKS.

We assume module 4 and the logic is very much for that.  Now if
we introduced a table that had clock face and value, that would
allow us to have different counter types.  The fonts that I have
allow for circles of 8 or 4 or 2 segments."
  (let*((track
          ;; String empty spaces in the track
          (replace-regexp-in-string "[[:space:]]+" "" track))
         (len
           (length track))
         (capacity
           (* len 4))
         (initial-value
           (cl-reduce
             (lambda (acc clock)
               (+
                 acc
                 (cond
                   ((string= "" clock) 0)
                   ((string= "󰪟" clock) 1)
                   ((string= "󰪡" clock) 2)
                   ((string= "󰪣" clock) 3)
                   ((string= "󰪥" clock) 4)
                   (t
                     (user-error
                       "Expected %s to be a quarter-increment clock"
                       track)))))
             (split-string track "" t)
             :initial-value 0))
         (updated-value
           (+ initial-value ticks))
         (remainder (mod updated-value 4))
         (filled-clocks (/ updated-value 4)))
         (concat
             (s-repeat filled-clocks "󰪥")
             (cond
               ((= 0 remainder) "")
               ((= 1 remainder) "󰪟")
               ((= 2 remainder) "󰪡")
               ((= 3 remainder) "󰪣")
               ((= 4 remainder) "󰪥"))
             (s-repeat (- len filled-clocks 1) ""))))
-1:-- Creating an Emacs Helper Function for Incrementing a Game Clock Track (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 03, 2024 11:41 PM

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

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!