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.
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.
)
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.
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.
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.
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.
)
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.
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:
(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.
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.
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.
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.
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.
)
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.
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
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
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
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
Technical Implementation
For the Bestiary, I’ve declared the following in the Property Drawer:
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.
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:
;;; 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:
Radio Links
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.
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.
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)
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.
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.
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 📖)
.
)
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.
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.
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.
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))
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.
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.
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:
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.
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:
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.
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:
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:
Emacs function to convert abbreviations to a DenoteSIGNATURE.
(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.
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.
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.
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.
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)
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.
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.
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.
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.
I started on that work and wanted to share a bit of the process.
Normalize my current feed.
Create a Todo oriented document.
Do the Todo.
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 Ctrl100F4.
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.
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:
I was defining a clear interface.
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:
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.
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 📖
.
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.
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.
Since I have mentored over 40 software developers; many that were in the process of changing careers into software development. I’ve also managed a couple of small software development teams.
Framing Approaches
Remember, we all don’t know what we don’t know.
Be curious while mentoring.
Ask questions.
Be visible.
Pair to share.
What Are You Looking to Learn?
When I start mentoring folks, I ask some form of the following question:
What have you been wanting to learn more of, get better at, and improve on?
Then I ask that they tell me more.
When we later meet, I like to use coaching questions:
What’s going well?
Where are you getting stuck?
If you could change one thing, what would it be?
Make the Work Visible
Like many people, I shifted to remote work in . I have noticed higher collaboration in remote work…when folks make their work visible.
I host office hours.
I try to attend other people’s office hours.
I’ll open up a Slack Huddle and code by myself; but let folks know they can hop in.
Hop In and Be Curious
I pay attention to other huddles that start; if they are still going after 45 minutes or so, I’ll hop in to say hello. It’s even odds that they are moving through the problem or are stuck.
By hopping into that Slack Huddle, I’m helping a common problem: How to know you’re stuck … and when do you ask for help?
The earlier you ask, the quicker the course correction; but too early and perhaps you don’t internalize the lessons of the struggle.
The later you ask, the more you’ll hopefully understand of the sticky problem; however you may have been needlessly added stress and fatigue because others could’ve helped.
Pairing is for Sharing
I tend to let others drive in pairing sessions. They’re typing and working to resolve the problem; and I’m there giving guidance. I’m also spending my time observing how they interact with their text editor. “In the moment,” I limit my advice to one concept; saying something like: control + a will take you to the beginning of the line.
I’ll gently mention that one thing once during our session.
And assuming we have a regular mentoring session, I’ll make sure to ask how they’re feeling about using their tools.
I’d love to get to the point where they ask me: “You saw me using my editor, what’s some things you think I could/should practice/learn?” I’m working on that.
Editor Functions
While pairing, I like to pay attention to how folks handle the following:
Where do they want to go?
How do they get there?
Here they are, now what?
How do they summarize?
I know what I can do with Emacs📖
; and I assume that Visual Studio Code (VS Code 📖)
can do something similar. It’s a matter of helping the mentees find those packages/plugins.
Where Do They Want to Go?
Search within a project
The subtle improvement of Orderless is under-valued
Search across projects
Cross-repository search is simple in Emacs
…and I’ve never seen someone do so in VS Code
.
I bind M-. to this; it hurts to watch folks use the mouse to do this simple jump.
File finders
consult-projectile is an amazing multi-function finder.
Jump between definition and test
I bind s-. a sibling of jump to definition; I want more folks to use the Ruby📖
/ RSpec📖
plugin in VS Code
.
Here They Am, Now What?
Word-completion
dabbrev, templates, hippie expand, and completion-at-point; it hurts to watch folks type
Auto-formatting
Tree Sitter📖
; I haven’t seen folks install VS Code
packages for auto-format.
Multi-Cursor / IEdit
It took a long-time to fully explore iedit; but the practice I did transformed my approach.
Inline searching
My beloved Textmate📖
introduced me to line filtering; to this day, I use that to understand the neighborhood of text around the text I’m searching.
How Do They Summarize?
I’ve seen a lot of boot camp graduates trained to write commit messages by going to the command line and typing git commit -m. They then use their command line to type out their message.
In my experience, commit messages written on the command line tend to be terse and missing useful context related to the commit. By shifting folks to use their text editor, we’re encouraging an interface where they can write more expressive messages.
Teach them about $GIT_EDITOR and $EDITOR; they’re making commits from the command line. At least help them learn to use their editor.
General Strategies
My goal is to encourage folks to use their editor for writing. To think about owning that tool.
Commit to One Item of Learning Each Week
There’s an absurd abundance of short-cuts, automation, and approaches to solving problems. Don’t overwhelm folks.
As folks are solving problems that “earn” their income, nudge them to practice one thing. That one thing is easier to slot into their everyday work.
Practice within Your Knowledge Domain
Any sufficiently advanced hobby is indistinguishable from work.
I also encourage folks to practice working on problems within a comfortable-to-them domain of knowledge.
I play table top Role Playing Games (RPGs 📖)
; and use that domain as the material for practicing coding. In years past, I’d write Ruby
code for dice rollers, note takers, random table lookups. Now I’m doing more of that in Emacs
.
Note Taking
Pay attention to how folks create a fleeting note. Some will be in their text editor and anguish about where to create a new file. Others will have a notes application running and write something there.
Help folks think about their note writing habits. Ask about it. Listen to their story and needs.
Help Them Navigate the Proprietary Software Tar Pits
One person was a dedicated Evernote user who has watched their digital contents evaporate. They are approaching their needs very differently than the person that wants their mobile and laptop to have synchronized data so they can have on the go notes that update.
My ideal state is for folks to use their text editor for note taking. Similar to commit messages. Through the language and driver of “getting better at using their text editor.” No need to learn different short-cuts or lament having one tool available in one ecosystem and not the other.
Help Show the Joy of Holistic Computering
Put another way, many folks have ceded different aspects of their digital life to specific and isolated applications. Less prevalent is the holistic thinking of a generalized “computering” environment. Folks need help learning what they don’t know, so they can choose how best to proceed for their lived experiences.
Playing is for Staying
I think one of the reasons I’ve remained a software developer is because I approach all of this as play and story-telling.
Some by-products of that play happens to be shipped features and documentation.
Yet I don’t tell folks to use Emacs
; instead I’m doing my best to show a myriad of reasons for why folks should consider Emacs
.
Conclusion
Ask questions of those you’re mentoring; namely how they are looking to improve. Know and show what your editor can do, so that you can make visible learning opportunities for those you mentor.
There’s a mid-level engineer on my team and we both play the “Hey, I learned something new, can I show you?” game. Most important…we play with our editors.
A secondary goal is showing the malleablity of Emacs
; how easy it is to extend.
And obviously there’s so much more than what I’ve highlighted. But…that’s Emacs
.
I’ve been employed as a software developer since . I’ve been writing for my personal blog since . In I switched to Emacs📖
; Previously Atom (Atom 📖)
, Sublime Text📖
, and Textmate📖
; with numerous failed forays into Vim📖
and a turn away in disgust of Visual Studio Code (VS Code 📖)
and it’s belittling treatment of real estate for writing commit messages.
Curious about my public writing habits, I added a Post Statistics page to my personal site. Prior to Emacs
, I had about a 95 words per day public output. After switching to Emacs
I’m now averaging 340 words per day.
Delta, Delta, Delta
There are many reasons for this change:
A pandemic removed my commute of 2 hours per day.
A pandemic shifted my habits towards more alone/contemplative time.
My children graduated secondary school and moved out.
My wife’s business became more self-sustaining.
I expanded the scope of my blog; from gaming towards everything that I want to publicly write about.
I changed employers; to one that encouraged public knowledge sharing.
All of this with Emacs
as my supporting technology.
I have extended myEmacs
to reduce barriers to capturing, synthesizing, referencing, and publishing my thinking and knowledge sharing.
And I’m always looking for refinements.
Well, How Did I Get Here?
I switched from WordPress📖
to Jekyll📖
then to Hugo📖
. When I switched from WordPress
to Jekyll
I fully adopted Markdown.
During that switch I was learning Emacs
; and hesitant to learn Org-Mode📖
. It seemed like too much to introduce.
I was still keeping my public writing separate from my personal/private writing.
Keep Em’ Separated
I didn’t realize how much that separation created mental friction in my writing:
Where should I write this?
If I want to switch from private to public, what might I need to sanitize?
Further complicating things, private stuff used one markup format; public stuff had different markup.
I wrote down a data flow diagram to help think through how all local writing and links could move to a world-viewable state without private information also bleeding out.
Put another way, I have done the “pre-thinking” about where things should go. Now I file them and can easily refile them.
Domains for Notes
For this presentation, there are four notable domains:
blog-post
I’m going to be publishing this.
epigraph
Meaningful quotes that I’ve gathered in my writing.
glossary
These are a term or concept.
melange
I don’t know where it goes, so it goes here.
It is trivial to move notes between domains.
Exploratory Exhumation and Exhilarating Exposition
I experimented with some of those Zettelkasten📖
approaches and began a journey of crafting and continuously refactoring my Personal Knowledge Management (PKM 📖)
process. What comes next is a demonstration.
In that time I’ve switched from the amazing Org-Roam
(by Jehtro Kuan) to the simplified (see less dependencies) Denote📖
package (by Protesilaos Stavrou (Prot 📖)
).
Demo Time
A stark difference for my preferred environment; be it writing or coding is that I don’t use:
tabs
file/directory tree navigator
multiple monitors
I aim to have one visible buffer; and split as needed. In other words, my hope is to minimize focal distractions.
Let’s get started.
Create a Note
I have mapped the H-d c prefix to the mnemonic “Denote Create…” something. Through the which key📖
package, I then show the functions for each of the “domains” I might write about.
Let’s start a blog post.
(Sidenote:
My blog makes extensive use of sidenotes and marginalia.
)
Dabbrev and Hippie Expand
After watching Jay Dixit’s Emacs for Writers presentation, I adopted Dabbrev for auto-correction. I also make use of Hippie expand for word completion.
While pairing with those that use VS Code
, I always wonder what tools they could use to match the simple functionality of the Hippie Expand and Dabbrev packages.
These are now so foundational, I don’t think about them. They just make writing easier.
Links
Let’s make a few links:
Let’s link to a note that does not have a “public” representation.
Let’s link to a note that references a “public” resource (e.g.
has a URL 📖
)
These will export as CITE and/or A tags.
Let’s convert a note link from a “title” to an abbreviation.
Abbreviations export as ABBR tags.
I also have a date: type link
Exports as TIME tag.
Let’s insert an epigraph.
Back-links
I want my notes to move towards the most relevant note, but be discoverable based on how my brain filed things away. With back-linking, I’m regularly leaving breadcrumbs to help find my way to the relevant information.
Moving Through the Thoughts
Put another way, links and back-links provide another means of discovery within my 3400 or so notes.
As are hand-crafted indices; file searches; file names; and tags.
Clocking
Sometimes, I’ll be writing and want to reference code (or prior blog posts). I’ll use Org-Mode
’s capture to clock functionality to help quickly gather references. There are other ways to approach this, but it’s what I have.
Macros
I use Daniel Mendler’s Tempel package. For Org-Mode
, I have a few templates that inject macro markup.
When I cite non-glossary entries, I use H-m c to insert a cite macro, and prompt for existing “cite” references in the document. Or C-u H-m c to prompt for all sources in my PKM
.
I have cite, keyboard, idiomatic, and mechanic that are all part of the H-m prefix group. These macros evaluate differently for Hugo
compared to LaTeX📖
.
These macros help me write more semantic HTML
.
Blocks
Org-Mode
’s default org-insert-structure-block is great, but I want further prompts. So I wrote a function that will take typing ``` as the first characters of a line and prompt for the type of block.
Let’s look at the “Blockquote” option. It prompts me for author, cite, and cite url. It builds from the org-insert-structure-block but prompts me to consistently fill out optional information.
These prompts also help me remember the syntax of writing these custom blocks.
Abstract
As I wrap up my writing, I include an abstract/summary. This is my chance for synthesis and to help possible future readers determine if this article is relevant to their interests.
It’s also what I use for the page’s meta description and eventually text for posting the link to Mastodon📖
.
When I first started blogging, I wanted to “publish” and didn’t take the time to summarize. Now, I consider it useful to “pitch” the document I wrote.
When I started learning Emacs
, I quickly shifted to “Vanilla” Emacs
and just started writing.
As I wrote, when I needed to do something that I’d previously done in a text editor, I’d find and experiment with different packages.
I continue with that mindset. As I write, I’m attending to what I’m doing. And eventually I realize “if I were to just write a function that does this one thing…I’d have a smoother writing experience.”
The goal is to minimize focal distractions, while also helping me create breadcrumbs that lead back to my writing and thoughts.
One of those “functions” I’d like to write is extending my abbr: export to work with LaTeX
; it’s halfway there I just need to prioritize and spend an evening of playing with Emacs
.
Вот некоторые принципы работы IDE (интегрированных сред разработки), которые, на мой взгляд, нужно зафиксировать:
Принцип минимальной навигации
Навигация в IDE раздражает. В тексте, конечно, можно выучить все причудливые навигационные сокращения типа C-a M-< M-f и так далее, но всё равно требуется немало набирать на клавиатуре. А если речь идет о навигации в файловой системе, то требуется еще больше ввода и поиска.
IDE должна минимизировать время, требуемое на навигацию. В частности, можно организовать автоматический импорт функции, когда пользователь набирает call-сайт, чтобы не переходить в верхнюю часть файла, чтобы добраться до импорта, или использование goto определения, чтобы не приходилось выяснять, в каком файле определен тот или иной класс.
Навигация также может быть связана с перемещением блоков кода, например, если вам нужно обратить оператор if, то это должно обеспечиваться на уровне кода, а не только при помощи манипуляций вручную.
Вы должны стремиться к тому, чтобы пользователи при наборе кода мыслили линейно. Вспомните, как современный траекторию набора текста, подобно тому, как современный ЦП-ориентированный код избегает ветвлений и переходов. Читать дальше →
I rarely use Git📖
submodules. My text editor workflow and mental heuristics assume that a “project” is one Git
repository. I use projectile (with some Consult decoration) to find files within the project. It works great.
(Sidenote:
Especially with the amazing consult-projectile package.
)
However in a new project Hyku📖
we’ve started using Git
submodules.
(Sidenote:
I’ll have more on this novel approach at some future point. For now, it’s a prototype for extending an existing Ruby on Rails📖
application.
)
With this change, when I look for a file via projectile I get a list that includes the files in the main repository and the files in the very large Git
submodule. Confounding things is that the file names are often quite similar.
I spent an hour or so delving into how to remove submodule files from the file selection.
First, I discovered that by default, projectile’s indexing method is alien; meaning it uses external shell commands to build the list. Looking at the source code, I found that the alien code was using fd📖
with the following args: -H -0 -E .git -tf --strip-cwd-prefix -c never.
(Sidenote:
Stored in the projectile-git-fd-args variable.
)
To understand what those all meant, I looked to the man page for fd. The critical switch was the -E .git. That meant to exclude all files from the .git directory.
What I wanted was to also exclude the hyrax-webapp directory i.e.
the Git
submodule’s root directory.
I added the following to my project’s .dir-locals.el:
And gave it a spin. I still saw the files. I did some further digging, and projectile also has discovery of files in Git
submodules. So I needed to configure that.
By default the shell command, stored in projectile-git-submodule-command is git submodule --quiet foreach 'echo $displaypath' | tr '\n' '\0'.
So to work around that, I amended my .dir-locals.el:
I was reading Errant📖
and got curious about the mathematic behind the fiddly Movement Dice. A character’s Movement Dice are derived from their physique, skill, and number of item Slots carried.
The Movement Dice is a proud and odd little rule that impacts movement in combat as well as chases. My conjecture is that the chase mechanic encourages you to throw down your backpack and run; multiple dice are advantageous for escaping.
Of important note, the only time a character’s movement dice matter is during a chase and during combat. Exploration is not based on the movement dice.
Errant’s Chase Procedure
First let’s look at the Chases section of Errant
:
In the case where they are being chased through a dungeon or similarly defined area, or for a short pursuit, the hunt can play out using standard initiative turn rules. However, for longer pursuits, and ones that may take place in broadly abstracted spaces like the wilderness or in cities, the following chase procedure can be used.
In a chase, generally, the participants can be tracked in terms of what side they’re on (i.e. pursuers and fugitives), but some chases may involve multiple parties or characters that need to be tracked separately.
Each initiative turn, the character with the lowest spd or mv on each side makes a movement roll. If the characters are on mounts or vehicles, use the spd of the mount or vehicle.
If the fugitives roll two 4’s, then they escape and the chase ends. If the pursuers roll two 4’s, they have caught the fugitives. In case of a tie, both sides make a movement roll as a tiebreaker.
If either side rolls doubles that are not 4’s, then characters on that side may make melee attack roll, perform a sorcery or miracle, or any other actions they wish.
If any of the results on both side’s movement dice match each other, characters may make ranged attack rolls against the other side. So if the fugitives rolled a 3 and a 4, and the pursuers rolled a 2 and a 3, the 3’s match, and so characters on each side may make ranged attack rolls against characters on the other side.
Dropping items during a chase is a free action. Dropping something the pursuers are interested in (food, money, etc.) may force a morale roll to see if they continue the chase.
Characters on either side may choose to sprint, rolling double their normal amount of movement dice, but they must make a physcheck with a dv equal to their encumbrance to do so. If they fail, they may not make a movement roll this initiative turn.
Characters on either side can choose to split off from their group; they will make movement rolls separately.
At the end of the initiative turn, if the chase has not yet ended, the side that rolled the lowest on their movement roll rolls a d10 for a chasedevelopment that affects them.
We have some high stakes dice rolls where having more movement dice increases your chances of ending the chase or taking meaningful actions during the chase. There’s a press your luck element in choosing to sprint (and garner potentially more dice). There’s also the option to split from the slower characters.
All modeling the absolute chaos of a good old fashioned rout.
On to the Mathematic of It All
With that in mind, we dive into the fiddly bits. We use two attributes (e.g.
physique and skill) plus the more fluid number of carried Slots.
Errant
encourages you to calculate your Spd while carrying your backpack and when not carrying your backpack.
(Sidenote:
The book wants to make it clear just how much that backpack is costing you during a chase.
)
A character’s ENC (or Encumbrance) is:
When slots > physique, then 4 + slots - physique
Else (4 × slots) ÷ physique rounded down.
A character’s SPD (or Speed) is skill - encumberance. And their movement die is speed + 4 rounded down.
Emacs Lisp Function
The following function prompts for information and provides movement dice while carrying your backpack and when not carrying your backpack.
(Sidenote:
I suppose it would also be useful to see the effect of discarding things carried in your hands. Because when you’re running maybe discarding that greatsword is a better plan than carrying the dead weight.
)
I like the chase procedure; and the fiddly bits make the procedure possible. Also given that combat might lead to chase, having a clear understanding of a character’s movement dice helps folks think about the viability of fleeing.
If I don’t want to worry about it, a quick and dirty method could be:
Pick the higher of physique or skill and subtract the slots.
Select the lowest sided-die from the dice chain that has sides greater than the above amount.
I started working with open-source software in . Having come from a Microsoft Windows and IBM System iSeries📖
environment, I was accustomed to my tools being vendor provided.
In I began my quest to find the software for editing my code. I looked into JEdit, TextWrangler, and BBEDit. I even tried Emacs📖
.
(Sidenote:
I’ve often wondered what would be different had I chosen Emacs
in . But back then, I didn’t have patience for a tutorial
)
While I was looking and using these different editors, I learned about Ruby on Rails📖
and saw folks using Textmate📖
.
I adopted Textmate
; it felt intuitive and in those earlier days of Ruby on Rails
it was the defacto editor of so many Rubyists.
To this day, the function of line filter is one that I continue to consider necessary for my editing experience.
(Sidenote:
In Emacs
, I use consult-lines to replicate this behavior.
)
In Textmate, I wrote a few snippets, but never a plugin. I did spend time crafting a refined Railscast theme.
Sometime in or I switched from Textmate to Sublime Text📖
. The primary reason for switching was Textmate’s slowness in searching within a large project. I brought forward my Railscast theme.
In , I switched from Sublime Text to Atom📖
. The foundational reason was that Atom was open-source. I again brought forward my modified Railscast theme.
(Sidenote:https://github.com/jeremyf/vibrant-ink-ruby)
I remember practicing Vim📖
but the paradigm did not stick.
(Sidenote:
I continue to practice on occasion, mostly because remote machines invariably have Vim installed.
)
I began exploring writing plugins and themes in Atom. I found it clunky as you needed to write configuration/functions in multiple files.
During , I began noticing that Atom was more prone to breakage. And I didn’t like the slow boot time for Atom; especially when I had my GIT_EDITOR configured to atom -w; I would type git commit on the command-line and wait for Atom to boot up so I could type a commit message.
In , I grew tired of Atom’s flakiness, and saw that with Microsoft’s purchase of Github that Visual Studio Code (VS Code 📖)
would likely be getting attention and Atom would further languish.
(Sidenote:
It was that Github announced they were Sunsetting Atom.
)
On I stopped using Atom and started using and once again practicing Vim
. Humans, I swear I have tried many times to learn and use Vim. I can fumble around, but modal editing does not work for me.
On , I stopped trying to make Vim my editor and started exploring VS Code
. It reminded me of Atom, Textmate, and Sublime. But there were two things I couldn’t get over:
The default window dressing felt like I might accidentally charge my credit card at some point.
The default commit message input is an HTML type=text element. A design consideration that encourages writing very little for commit messages.
I’ve since learned that configuring the window dressing is somewhat trivial. But that pernicious commit message input remains.
And why was that a big deal? My thinking was that any code editor that has a small input area for entering commit messages was built on the principles that history does not matter.
And would propagate subtle bad habits; such as modeling that folks need not write good commit messages. Good commit messages help expose intent and constraints.
I set aside VS Code
and looked for other options. Emacs
was barely a consideration. But I was running out of viable options.
I spent time practicing, namely working through the tutorial and exploring package archives to find utilities to help me write.
On I switched to Emacs. I tried both Doom and Spacemacs📖
, but settled on building my own configuration.
What this looked like was starting from a baseline install and just writing and coding. When I hit a point where I knew past editors did something, I would begin exploring.
(Sidenote:
I did this with the mindset of “I am learning and building my tools; so other things will move slower.” In other words, if I had an urgent task to complete, I wouldn’t do this. But if I had some slack in time, I’d take time. Also it was ; I had lots of slack time.
)
The most fruitful exploration came when I wanted to use the line filter function that was part of Textmate, Sublime, and Atom.
This lead me to Ivy, Counsel, and Swiper; this trio helped me understand Emacs. I could see the interactive functions and their first line doc-strings. That discoverability, paired with Helpful helped me build steam.
Eventually, I switched from the trio of Ivy, Counsel, and Swiper to the quintet of Consult, Marginalia, Vertico, Orderless, and Embark. The reason? The package sizes and design considerations. Namely the quintet of packages built atop the Emacs Application Programming Interface (API 📖)
; creating what is likely to be less maintenance overhead.
As I watch folks use their editors, I’ve discovered one small bit of functionality that Emacs has versus other editors. The concpet of a buffer, separate from the file. I can create a Scratch Buffer to write down a thought or bit of code. I don’t need to fiddle with creating a new file and deciding where that file goes. The Scratch Buffer serves as a somewhat durable place that favors capturing the idea over determining where to file that idea.
I know for myself, if I’m working on something then have an idea partially related, I would lose a lot of concentration and focus if I also had to specify where I write that idea.
Conclusion
These days, I struggle to think of what would prompt me to move from Emacs. I’ve created an environment that works for me; and when I begin to see repeated tasks I spend a bit of time exploring ways to encode them.
Creating functions and packages in Emacs feels far less arduous than it did in Atom and VS Code
. I’m extending my editor at a slower frequency, but that’s a function of maturity.
On Thursdays I often have dinner and drinks with my good friend Brandon. We talk about books, music, Zelda, travel, general programming, and Emacs📖
. He’s been using Emacs
for decades, whereas I’m a recent convert.
(Sidenote:
Since .
)
This we were again talking about Emacs
. And Brandon wondered the following: “It would be interesting to have a set of tasks we needed to accomplish in the same document, and see how differently we approach the solution.”
We haven’t ever done any pair programming together, but know that our approaches to navigating and editing are different. Because Emacs
is an editor that flows and moves with you. And Brandon and I adopted Emacs
more than two decards apart; so his habits and approaches will likely vary from mine. In part because of the available packages we’ve each adopted and practiced.
More than any other editor I’ve used, I spend time honing and practicing Emacs
.
(Sidenote:
Part of this is the joy of discovery and thinking through different approaches.
)
I’m a recent convert to Emacs (started in 2020). I’ve been a professional software developer for years. I gave VS Code a whirl back in 2020, but it didn’t suit me.
What I like about Emacs is that I have an integrated computing environment: writing, reading, and coding. When I get better at using my tools, all aspects benefit. At https://takeonrules.com/about/stats/ you can see an overview of my pre-Emacs stats versus my with Emacs. It’s a game changer for me.
The other bit of Emacs that I adore is how quick it is to extend my editor. I can record a macro or write a function, and save that little snippet of code for future usage. It’s easy to temporarily assign a key combination to a function.
I wasn’t aiming to convince the original poster but instead to get them thinking about their editor.
A question I like to ask the people I mentor, “How many more years do you think you’ll be writing code? Writing in general? Do you think at least for the next 5 years?” All of that is a nudge for them to practice using their tools. A reminder that the tools we use shape what we view as possible or feasible.
This week I’ve been wrestling with an integration environment challenge. I need to debug code on a remote demo instance that is configured similar to production. Due to the current constraints, this involves using Secure Shell (SSH 📖)
to edit remote files in a Kubernetes (K8s 📖)
cluster, then restarting the web-server for the cluster, and finally checking logs to see what’s happening.
To say it is tedious is an understatement.
Initial Solution
Further confounding the problem was that I needed to make these changes using Vim📖
. I like Vim
but my Emacs📖
key bindings continually get in the way.
My initial work around was to create a simple interactive Emacs
function:
(defun jf/rancher/rm-then-vim-project-file (&optional filename)
"Kill some text to edit a FILENAME in Rancher."
(interactive)
(let* ((f (or filename (buffer-file-name)))
(relative-name
(concat "./"
(file-relative-name f (projectile-project-root)))))
(kill-new (f-read f))
(kill-new (format "rm %s ; vim %s"
relative-name relative-name))))
The above copies the current file contents. Then copies the command to remove the old file and edit the file in Vim
. The idea being that while I’m in the web User Interface (UI 📖)
, I can past the rm ; vim command to open a clean file and then paste the contents into it.
(Sidenote:
It’s a bit of a lazy-person’s scp
.
)
Let’s Go Tramp
I have wanted to be able to SSH
into a K8s
pod. I spent a bit of time and these are my steps for leveraging Emacs
’s Tramp package. We use Rancher📖
for our K8s
management.
brew install fzf (for some fuzzy finding friendlyness)
kubectl krew install ctx
kubectl krew install konfig
kubectl krew install ns
Ensure that ~/.krew/bin is in your $PATH
Ensure that ~/.kube/config file exists (e.g.
touch ~/.kube/config)
Rancher
Copy the contents of Rancher’s cluster’s KubeConfig into a local file.
(Sidenote:
As of the config is available in the top-right menu of the cluster.
)
For this example, we’ll write the file to ~/kubeconfig.tmp.yml
With that file, add the config to your kubectl (e.g.
kubectl konfig import --save ~/kubeconfig.tmp.yml).
Run kubectl config get-contexts; you should see the cluster you just added to the kubectl konfig.
Run kubectl ctx and set the current context to the cluster you added.
Run kubectl get pods; you should see the named pods. In the case of our Hyku📖
builds you’ll see something along the lines of the following:
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
palni-palci-demo-nginx-66d86c56c5-pdzn4 1/1 Running 0 11h
palni-palci-demo-pals-84bc48c974-64ztz 1/1 Running 0 69m
palni-palci-demo-pals-84bc48c974-pnvnn 1/1 Running 0 67m
palni-palci-demo-pals-worker-c54c8d69d-pd9p4 1/1 Running 0 69m
palni-palci-demo-redis-master-0 1/1 Running 0 6d7h
Tramp-ing to Those Files
With all of that pre-amble, in EmacsM-x find-file and type /kubernetes:palni-palci-demo-pals-84bc48c974-pnvnn:/. The trailing :/ indicates that we’re looking in the default directory, and with a Rails application you’ll see the usual suspects for directories (e.g.
app/lib/ etc.); begin navigating and editing those files.
If I find myself doing this quite regularly, I’ll likely want to create some helper functions for the find-file.
, I extended the existing functionality with two features:
Exclude a table from prompting for a roll.
Allow for rudimentary math operands with table results.
Exclude a Table from Prompting for a Roll
Prior to my most recent change, when you pass the universal prefix arg to random-table/roll it would always prompt for rolls on the table. Even if there was one record in the :data element. You would get a prompt that looked like: “Roll 1d1 on Your Named Table:”
With the most recent change, any table that has one element in :data will not prompt for the roll. Also, you can specify :exclude-from-prompt t when registering a table; then any “rolls” on that specific table will not prompt to give the dice value.
Ultimately, the goal is to ask for dice rolls when they might be something the player wants to roll.
Allow for Rudimentary Math Operands with Table Results
In my quest for more random tables and functionality, I worked through Errant📖
’s Hiring Retainers section. Using the Player Character (PC 📖)
’s presence, you look-up the morale base. Then roll 2d6, modified by the offer’s generosity, to then determine the modifier to the morale base.
To perform mathematical operations, I continue to leverage the s-format functionality. That is s-format will evaluate and replace the text of the following format: ${text}.
Below is the definition of a random Henchman for Errant.
(Sidenote:
I do need to consider randomizing it’s level; higher level henchmen are available at larger urban centers.
)
Adding this functionality was somewhat quick.
(Sidenote:
I stumbled on the match-string-no-properties; when I did not pass the text it used the positional data of string-match to look at the buffer’s text. In other words, it was broken without this.
)
Below is the code that I added for replacer function logic. The \\[\\(.*\\)\\][[:space:]]*\\(-\\|\\+\\|\\*\\)[[:space:]]*\\[\\(.*\\)\\]Regular Expression📖
is a lot to visually parse.
There are three capture regions:
First: The text within square brackets that is to the left of the operand (e.g. second capture region)
Second: The mathematical operand (e.g. -, +, or *; no division just yet)
Third: The text within square brackets that is to the right of the operand (e.g. second capture region)
We use Emacs📖
’s apply function to call the operand as a function. The left operator being the rolled results of the first capture region. The right operator being the results of rolling the third capture region.
Having mathematical operators assumes that the tables are returning number like values.
When registering tables, I now disallow [ and/or ] in the :name of the table. This way I can be more confident that the ${[Table] + [Table Two]} will result in operand logic.
Conclusion
Part of my ongoing reason for doing this is to build up my “Game Master (GM 📖)
notebook” of tables and procedures. In my experience, any of these tables might be useful for inspiration or reducing rules lookup during play.
В этой статье мы разберём использование в Emacs различных команд для упорядочивания строк, абзацев и страниц. В руководствах к Emacs и Elisp эти команды описаны достаточно подробно, так что мы просто познакомимся с практическими примерами их применения. Читать дальше →
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!