Сложно найти человека, про которого ходит больше легенд, чем Ричард Столлман (RMS). Наверное, по количеству невероятных историй и анекдотов с ним может сравниться только Джефф Дин.
В 71 год Столлман борется с раком (успешно) и пострадал от травли за свои спорные высказывания и действия. Но его достижений хватит на десятки или сотни великих жизней. Он является автором компилятора GCC и текстового редактора Emacs, а также основателем проекта GNU, дополнением к которому стало ядро Linux, в результате чего появилась полноценная ОС.
Гений Столлмана опередил время. Только в 21 веке — с ползучим наступлением цифрового тоталитаризма, всеобщей слежки в духе «1984» и алгоритмизацией всех аспектов жизни — люди постепенно начинают понимать, что Ричард Столлман был прав. Читать дальше →
Одна из проблем GNU Emacs заключается в сложности создания воспроизводимой сборки. Разумеется, вы всегда можете сделать себе контейнер, и запускать Emacs с помощью Docker, Podman или даже Kubernetes... Но я сейчас не о таких сложных случаях.
Итак, суть проблемы: если создать файл init.el и отладить его, то через 2-3 месяца запуск с ним Emacs на другом компьютере с большой вероятностью приведёт к ошибкам установки или несовместимости пакетов.
В этой статье рассматривается использование возможностей встроенного пакета package-vc-install.el для решения описанной проблемы.
I make extensive use of Org-Mode📖
’s linking functionality.
(Sidenote:
See External Links (The Org Manual).
)
, I quickly
extended the functionality of elfeed📖
links to include an :export option.
Use Case
While reading my RSS 📖
in elfeed
I might want to link to that entry in another
document. I can save a link to the current feed entry by invoking
org-store-link.
(Sidenote:
See Handling Links: org-store-link (The Org Manual).
)
Then in the other document, I can invoke org-insert-link.
(Sidenote:
See Handling Links: org-insert-link (The Org Manual).
)
This will insert a
link with a elfeed protocol and encode either an entry or a filter. In the case
I’m looking to handle, I want to export an entry not as an elfeed link but
instead to resolve that link to its origin.
I looked to the existing elfeed-link package and found the useful regular
expression for parsing the link, and it was a quick matter of writing up the
format pcase logic.
I added a new feature to my Random Tables package: the ability to prompt for a label and pick options. These are two separate features but related by their use case.
First let’s look over the Use Case, then at the Random Tables documentation.
Use Case
In Solo Gaming, one concept is the idea of asking an open-ended question, then consulting an oracle. Consulting an oracle often involves rolling on one or more tables based on the nature of the question.
Examples of questions and their choices are as follows:
“What is the nature of these woods?” (Aspect)
“What mission does my patron have for me?” (Action,
Aspect, Focus)
In the above example, I ask an open-ended question and then consult one or more additional tables.
Documentation
Below is an example of a table that will:
Prompt for a :label.
Prompt to pick one or more :options and from the chose options parse the
selected option(s).
(random-table/register :name "The One Ring > Lore"
:data '("{The One Ring > Lore @label}{The One Ring > Lore @options}")
:label '(read-string "Question: ")
:options '(("Action" . "- Action :: {The One Ring > Lore Table > Action}")
("Aspect" . "- Aspect :: {The One Ring > Lore Table > Aspect}")
("Feature" . "- Feature :: {The One Ring > Lore Table > Feature}")
("Focus" . "- Focus :: {The One Ring > Lore Table > Focus}")))
The syntax is {<TABLE_NAME> @label} to apply the :label function.
For parsing :options you can use {<TABLE_NAME> @options} for multi-select and
{<TABLE_NAME> @option} for singular select. The parsed options are prefixed
with \n\t.
Let’s say we roll on the “The One Ring > Lore” table ask the following question: “What are the orcs doing?”. We then choose the Action option which will roll on the “The One Ring > Lore Table > Action”. Which would output something similar to the following:
What are the orcs doing?
- Action :: Chase
Were we to select Action and Focus we’d get something similar to the following output:
What are the orcs doing?
- Action :: Chase
- Focus :: Luck
As I’m leveraging a struct for the table definition, I’m considering adding an :extensions field and then a general extension reducer, in which each extension would have it’s on parser function.
What I have works for now, and I’m pleased that it took about 30 minutes to identify the use-case, a syntax, and extend it.
Over the last few months, I’ve been writing Go Lang📖
code. I use the eglot package for Language Server interaction.
What I like about eglot is that it wires into built-in Emacs📖
packages. For “popup” documentation, eglot integrates with the eldoc📖
package. In my setup the results from the gopls📖
language server would often show what looked like links but were not clickable.
When I hovered with my mouse, I would see the URL to the documentation. But there was no bound action to text.
As a long-time user of Link Hint, I figured I would explore adding a new handler.
First, I started exploring some of the existing handlers. Many of them looked for propterties on the rendered text. I inspected the text properties of what looked like a link and saw that the help-echo property contained the URL.
;; Allow for opening the URLs provided in eldoc. This is rough and
;; minimal error handling.
(link-hint-define-type 'eldoc-url
:next #'link-hint--next-eldoc-url
:at-point-p #'link-hint--eldoc-url-at-point-p
:open #'browse-url
:copy #'kill-new)
(defun link-hint--next-eldoc-url (bound)
"Get position of next `face' at or after BOUND."
;; While we're interested in the 'help-echo value, we need to see if
;; we can't solely work from that. Instead we need to check if we
;; have a link face.
(link-hint--next-property-with-value 'face 'markdown-link-face bound))
(defun link-hint--eldoc-url-at-point-p ()
"Return the name of the eldoc link at the point or nil."
;; Mirroring `link-hint--next-eldoc-url' logic, when we have a link
;; face look to the help-echo for the URL.
(when (eq (get-text-property (point) 'face)
'markdown-link-face)
(get-text-property (point) 'help-echo)))
(push 'link-hint-eldoc-url link-hint-types)
Now, when I invoke M-x display-local-help, I can then invoke M-x link-hint-open-link which will include the inline documentation text as a candidate for opening a link.
update
My initial implementation was too greedy; namely it was finding other help-echo properties and clobbering those linking abilities. This solution is more specific, but still might benefit from a URL verification on that help-echo; but that is a future exercise. What I have is adequate.
Over the last month, I’ve started working in a rather large code-base. And I’m using the eglot package to help navigate to and from code definitions; under the hood eglot connects into the xref package, a tool for navigating to programatic symbols.
The xref package exposes several functions, creating a bit of a breadcrumb trail:
xref-find-definitions
Find the provided symbol’s definition; usually this is the symbol at point.
xref-go-back
Go back to the previously found symbol.
xref-go-forward
Go forward to the next found symbol; available when you went back.
And on some of my code expeditions, I jump to a definition, then another definition and so forth. It can be easy to lose my place; even as xref tracks where I’ve been.
But to my knowledge, the breadcrumbs aren’t visible…until I wired up a Consult function.
(defvar consult--xref-history nil
"History for the `consult-recent-xref' results.")
(defun consult-recent-xref (&optional markers)
"Jump to a marker in MARKERS list (defaults to `xref--history'.
The command supports preview of the currently selected marker position.
The symbol at point is added to the future history."
(interactive)
(consult--read
(consult--global-mark-candidates
(or markers (flatten-list xref--history)))
:prompt "Go to Xref: "
:annotate (consult--line-prefix)
:category 'consult-location
:sort nil
:require-match t
:lookup #'consult--lookup-location
:history '(:input consult--xref-history)
:add-history (thing-at-point 'symbol)
:state (consult--jump-state)))
This function behaves very much like Consult’s consult-mark function; showing a preview of each of those xref locations.
As part of my new job, I’m working in Go Lang (Go 📖)
and writing Swagger 2.0 specifications. We use Go Swagger📖
to generate code from the specification. This results in lots of files that have the preamble: “// Code generated”.
When searching the contents of a project, I often want to exclude those files.
I created jf/rg-get-not-generated-file-names; it would find all of the files that:
did not have the phrase //Code generated
were not in the ./vendor directory
(defun jf/rg-get-not-generated-file-names ()
"Create a custom type glob for files that were not generated."
(format "{%s}"
(string-trim
(shell-command-to-string
(concat
"rg -e \"^// Code generated .*DO NOT EDIT\\.$\" . "
"--files-without-match --glob=\\!vendor | tr '\\n' '\\,' "
" | sed 's|,$||' | sed 's|\\./||g'" )))))
The function transforms those files into a suitable glob format, which I can then specify as the :files value for the rg-define-search macro.
(rg-define-search rg-project-not-generated
"Search only within files that are not generated."
:files (jf/rg-get-not-generated-file-names)
:dir project
:menu ("Search" "n" "Not Generated"))
And just like that, I have an option to search within the non-generated files of my project; that is files that a human has more likely crafted.
Alternate Approach
The above functions work within the constraint of rg.el. When I’m on the command line, I can do the following:
I’ve been playing a solo game of Ironsworn: Starforged📖
. Follow
session reports if you are interested.
I’m using Emacs📖
for tracking that campaign, and exporting those sessions to blog posts. While playing I often find myself needing to review information.
I set about writing a function to help me review those notes by rendering a summary of the headlines with matching tags.
I also set about writing this function to be generally useful.
Organization of My Notes
The following represents most of the hierarchy of my notes; with the words in parentheses indicating the tags.
Character (#characters)
Attributes (#attributes)
Assets (#assets)
Vows (#vows)
Legacy Tracks (#tracks #legacy)
Expedition Tracks (#tracks #expeditions)
Clocks (#clocks)
Connections (#connections)
Factions (#factions)
Spaceships (#spaceships)
Sectors (#sectors)
Sector
Systems (#systems)
Settlements (#settlements)
Sessions (#sessions)
Below each of those headlines are additional detailed headlines. Important in this is that all headlines inherit the tags of their ancestor headlines.
Within each headline, I might write properties; the character’s momentum attribute for example. Or the progress track for a vow.
There’s quite a lot of information that I might need to cross-index while playing.
The Functions of Summarization
I ended up writing two functions:
jf/org-mode/buffer-headline-tags
Create a list of all non-file level tags.
jf/org-mode/summarize-tags
Prompt for the tag or tags to summarize then render a summary of headlines that match those tags.
The summary is itself an Org-Mode📖
document that shows each headline and the relevant properties; suitable for quick scanning.
(defun jf/org-mode/buffer-headline-tags ()
"Return a list of `org-mode' tags excluding filetags.
In the present implementation, I'm relying on `denote'
conventions. However, by creating a function I'm hiding the
implementation details on how I get that."
;; This is here to indicate the dependency
(require 'denote)
(let* ((all-tags
(org-get-buffer-tags))
(file-level-tags
(denote-extract-keywords-from-path (buffer-file-name))))
;; Given that I want inherited tags and the filetags are
;; considered to be on all headlines, I want to remove those tags.
(cl-reduce (lambda (mem el)
(if (member
(substring-no-properties (car el))
file-level-tags)
mem
(add-to-list 'mem el)))
all-tags :initial-value '())))
(defun jf/org-mode/summarize-tags (&optional tags)
"Create `org-mode' buffer that summarizes the headlines for TAGS.
This reducing function \"promotes\" the property drawer elements
to list elements while providing the same functionality of an
`org-mode' buffer.
Some of this could be accomplished with column/table declarations
but the headlines and content might not fit so well in the
buffer."
(interactive (list
(completing-read-multiple
"Tags: "
(jf/org-mode/buffer-headline-tags) nil t)))
(require 's)
;; With the given tags map the headlines and their properties.
(let* ((prop-names-to-skip
;; This is a list of headline properties that I really
;; don't want to report. I suspect some may be buffer
;; specific. But for now, this should be adequate.
;;
;; Perhaps in later iterations we'll prompt for additional
;; ones to ignore.
'("ID" "ALLTAGS" "FILE" "PRIORITY" "ITEM" "TIMESTAMP"
"TIMESTAMP_IA" "CATEGORY" "TAGS"
"BLOCKED" "TODO" "CLOSED"))
(text-chunks
;; In using `org-map-entries' I can access inherited tags,
;; which I find structurally useful
(org-map-entries
;; Yes this could be its own function but for now, we'll
;; leave it at that.
(lambda ()
(let* ((h (org-element-at-point))
;; Rebuild a terse header: depth, todo, title
;; only
(header-text
(format
"%s%s %s\n"
(s-repeat
(org-element-property :level h) "*")
(if-let ((todo
(org-element-property
:todo-keyword h)))
(format " %s" todo)
"")
(org-element-property :title h)))
;; Only select relevant properties, converting
;; those properties into a list of strings.
(properties-text
(cl-reduce
(lambda (mem prop-value)
(if (member (car prop-value)
prop-names-to-skip)
mem
(add-to-list 'mem
(format "- %s :: %s"
(car prop-value)
(cdr prop-value))
t)))
(org-entry-properties h)
:initial-value nil)))
;; If we have properties we want to render, we'll
;; have one format.
(if properties-text
(format "%s\n%s\n" header-text
(s-join "\n" properties-text))
header-text)))
(s-join "|" tags)
'file 'comment))
;; Let's have only one of these
(buffer-name "*Org Mode Tag Summary*")
(display-buffer-mark-dedicated t))
;; We've run this command again, so let's destroy what we had
;; and start anew.
(when (get-buffer buffer-name) (kill-buffer buffer-name))
(get-buffer-create buffer-name)
(with-current-buffer buffer-name
;; Minimize the chatter of the mode-line
(let ((mode-line
(concat
(propertize (format "%s Tags: #%s"
;; Show a lock icon
(char-to-string #xE0A2)
(s-join " #" tags))
'face 'mode-line-buffer-id)
" "
(propertize
"C-c C-k to exit"
'face 'jf/mode-line-format/face-shadow))))
;; This came from `org-mode' so let's continue to keep it that
;; way.
(org-mode)
(insert (s-join "\n" text-chunks))
;; Let's not have the illusion that we're allowing ourselves
;; to edit this text
(read-only-mode)
(pop-to-buffer buffer-name
`((display-buffer-in-side-window)
(side . right)
(window-width 72)
(window-parameters
(tab-line-format . none)
(mode-line-format . ,mode-line)
(no-delete-other-windows . t))))))))
Conclusion
While I envisioned these functions to help facilitate Role Playing Game (RPG 📖)
sessions, I know I’ll get use out of jf/org-mode/summarize-tags for some of my non-gaming notes.
Now, maybe instead of writing tools to help facilitate playing a game, I should sit down and play a game.
, afternoon I was pairing with a colleague. I was driving as she was helping navigate. Both of us were unfamiliar with the code-base, but she was more familiar with the technical domain.
(Sidenote:
She had done a bit of Angular, but quite a bit of React. Meanwhile, I was at hour 6 of my “break fixing” Angular journey.
)
Taking Private Time to Think
There were times where she would tell me “I need to orient to this code-base; I’m going to disconnect and re-join in 10 minutes.” In the moment, I thought that was a unique response, but I thought about it more: she needed time to use her tools to “load” a better working understanding of the project.
During one of those moments when she went to do her own research, I started down an exploratory thread that required me to have open four different files:
Component A
Component A’s button sub-component
Component B
Component B’s button sub-component
Why? Because Component A and Component B had similar functional expectations, but when I clicked the Component A button it would reset data on the web-page. I did not see that behavior when I clicked Component B’s button.
I started reading and refactoring, as I saw how these two components were constructed differently. I had my editor configured such that:
Component A in top left
Component A’s button in bottom left
Component B in top right
Component B’s button in bottom right
Most of the times when I’m coding, I have one or maybe two files open at the same time. This four-way split is very infrequent. But here I was using comparisons to help guide a refactor towards similarity.
Return to Pairing
And then my colleague hopped back on and she was ready to try something. In that moment, I realized “I haven’t learned how to save window state for later re-use.”
Not wanting to delay our pairing time, as we were coming up on 5pm on a Friday afternoon, I hid that context and opened a new one. Yet a little gremlin in the back of my head kept nagging me that I might lose that window context.
We continued pairing and got to a point where I had what I think to be enough to proceed on my own.
(Sidenote:
A problem I left for Jeremy to deal with.
)
Listening to that Little Gremlin
But I listened to that little gremlin and made a mental note to practice and learn two things:
How to save the configuration of windows in Emacs
.
Swap Location of Windows
I’ve long used the Ace Window📖
package to quickly jump from one window to another. I have also used Ace Window
to flip the location of two windows that are the only windows in a frame.
So I knew there was a way to swap the location of one window with another window when there were more than two windows in the frame.
I explored the documentation a bit more, and found ace-swap-window, which can be invoked within the M-x ace-window command.
This would help me better reconstruct the aforementioned four window arrangement; as I could re-arrange later.
Save Configuration of Windows
The other issue, namely preserving windows state over the weekend required a different approach. I know that I can use M-x window-configuration-to-register, and did in the moment. However, in my experiments this was not as reliable as I’d hoped.
(Sidenote:
I got some buffer gone errors as I moved about my business.
)
As a regular subscriber of Sacha Chua’s blog, I had seen project-butler roll across my feed earlier this week. I did some reading, at it wasn’t quite what I was looking for. But it did mention (and remind me of) the Activities📖
package.
I did some quick reading, and it looked like what I was after. So I installed it and started exploring.
And it is exactly what I want for that situation and perhaps others. It allows me to take a brief moment before switching contexts to consider bookmarking my current activity.
Conclusion
I’ve done a lot of pairing over the years and have often used pairing as a chance to learn other people’s approaches. I found it inspiring that in the moment of our pairing, my coworker asked and took a few breaks to further investigate. She came back with greater context and we moved quickly to isolating the specific problem.
I also took that time to observe that I had created a situation for myself that I wanted to learn how to better navigate, namely having a context that I wanted to “stash” and re-join our shared context.
I spent time reflecting on both of these moments and took time to practice and reflect on my observations.
As part of my new role, I’m working on a code base that is new to me. It’s also in a language that I’m familiar with but by no means an expert: Javascript. And the framework chosen is Angular. All told a lot of new concepts.
I have a task at work that requires fixing a bug; and it’s confounding as it’s related to mutating state.
In order to orient, I use the capture to clock concept. In my projects file, I created a headline (e.g.
“Working through UI Reset Bug”). Then I start a clock for that headline. And I begin my code exploration.
When I find a bit of code to consider, or that I want to “find again”, I invoke org-capture(Sidenote:
See Capture (The Org Manual).
)
and pick my “Capture to Clock”.
(Sidenote:My “Capture to Clock” template is defined here.
)
This grabs the current selected region and creates a link to the remote repository. I then add some more information about why it’s important and then complete the capture.
Prior to , I would also include a link to the local file. But I removed that. In part because I would often times export that headline as a PDF or Markdown and share with teammates; and they likely didn’t have those files on their machine.
I still wanted to open those code blocks locally in Emacs📖
over opening the remote document. Which lead me down a bit of exploration.
Link Hint Package
I have for quite some time used the Link Hint package. I love it. I invoke M-x link-hint-open-link and am presented with several overlays of characters. Each overlay is position above a “link” that Emacs
knows how to handle.
(Sidenote:
Which is a lot of things.
)
I type the character combination to select the link I want, and it opens the link in the “correct” application. In the case of https, it defaults to using my browser…until .
I added some advice that will intercept handling any Org-Mode
link and test if the link’s URI is a Github URL.
If it is, I test if I have that repository on my local machine. And when I do, I jump to the local file.
Below is that code:
(defun jf/link-hint--apply (advised-function func &rest args)
"Hijack opening `org-mode' URLs by attempting to open local file.
The ADVISED-FUNCTION is the original `link-hint--apply'.
The FUNC is the dispatched function for handling the link
type (e.g. `link-hint--open-org-link').
The ARGS are the rest of the ARGS passed to the ADVISED-FUNCTION."
(if (and
(eq 'link-hint--open-org-link func)
(eq :open (caddr args)))
(progn
(if-let* ((url
(car args))
(_match
(string-match
(concat "^https://github.com/"
"\\([^/]+\\)/\\([^/]+\\)" ;; org/repo
"/[^/]+/[^/]+/" ;; blob/sha
"\\([^#]+\\)" ;; path to fil
"\\(#L\\([0-9]+\\)\\)?") ;; line number
url)))
;; Due to my present file structure I have some repositories
;; in ~/git/ and others in ~/git/sub-dir
;;
;; In most every case, the Github org and repo match the
;; remote URL.
(let ((filename-without-org
(format "~/git/%s/%s"
(match-string 2 url)
(match-string 3 url)))
(filename-with-org
(format "~/git/%s/%s/%s"
(match-string 1 url)
(match-string 2 url)
(match-string 3 url)))
(line-number
(match-string 5 url)))
(cond
((f-exists? filename-without-org)
(progn
(find-file filename-without-org)
(when line-number
(goto-char (point-min))
(forward-line
(1- (string-to-number line-number))))))
((f-exists? filename-with-org)
(progn
(find-file filename-with-org)
(when line-number
(goto-char (point-min))
(forward-line
(1- (string-to-number line-number))))))
(t (funcall func args))))
(funcall func args)))
(apply advised-function func args)))
(advice-add 'link-hint--apply
:around #'jf/link-hint--apply)
As always, there’s room for refactor. But this gets the job done.
Conclusion
When exploring a code-base, I’m finding it helpful to annotate my explorations and be able to refer back to those findings.
With this change, I can continue to do that without sharing documents that have extra chatter regarding “local links.”
I used the above process (with local links) to add my working notes to a Github issue. I intended for those notes to be a way-finding tools for other folks who might come along; or for someone who might help guide me in a more fruitful direction.
Post Script
I’ve also used the process of capturing to a clock to create blog posts as well as run-book pages. In fact, for this post I started a clock on the Link Hint Package heading and then used my capture process to grab the code.
I’ve been playing a solo game of Ironsworn: Starforged📖
. Thus-far I haven’t published any of the sessions, but I’m looking to do so.
One of the concepts of Ironsworn: Starforged
is the track. Each track has ten boxes and each box can have 0 through 4 marks. When a box has 4 marks, it is considered full.
I find a much nicer representation than 6 of 40. But 6 of 40 is “easier” to do math against.
What follows is an Emacs function that will increment a track (e.g.
) by a number of ticks (e.g. 3). And it returns .
(defun jf/gaming/clock-track-incremement (track ticks)
"Return a TRACK incremented by a number of TICKS.
We assume module 4 and the logic is very much for that. Now if
we introduced a table that had clock face and value, that would
allow us to have different counter types. The fonts that I have
allow for circles of 8 or 4 or 2 segments."
(let*((track
;; String empty spaces in the track
(replace-regexp-in-string "[[:space:]]+" "" track))
(len
(length track))
(capacity
(* len 4))
(initial-value
(cl-reduce
(lambda (acc clock)
(+
acc
(cond
((string= "" clock) 0)
((string= "" clock) 1)
((string= "" clock) 2)
((string= "" clock) 3)
((string= "" clock) 4)
(t
(user-error
"Expected %s to be a quarter-increment clock"
track)))))
(split-string track "" t)
:initial-value 0))
(updated-value
(+ initial-value ticks))
(remainder (mod updated-value 4))
(filled-clocks (/ updated-value 4)))
(concat
(s-repeat filled-clocks "")
(cond
((= 0 remainder) "")
((= 1 remainder) "")
((= 2 remainder) "")
((= 3 remainder) "")
((= 4 remainder) ""))
(s-repeat (- len filled-clocks 1) ""))))
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.
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!