Jeremy Friesen: Migration Plan for Org-Roam Notes to Denote

Laying Out a Game Plan

Building on Exploring the Denote Emacs Package, I want to layout a migration plan. For the migration to be successful, I have the following criteria:

Glossary Migration
When I pull glossary entries into Take on Rules I expect the ./data/glossary.yml to be functionally identical to it’s current state. That is to say no nodes dropped nor added and keys being similar. I would go so far as to say after the migration the pull event should result in no change to ./data/glossary.yml.
Blog Export
I am able to export an Org-Mode 📖 format Denote file for publication on Take on Rules.
Data Structures
I am able to organize and discover notes via different attributes.
Keywords and Controlled Vocabularies
I spent time establishing the tags I use for blogging; I’d want assistance in adhering to that exercise.
Other Stuff
As I write this document, I’m certain other things will emerge.

Feature Tests

These are my guiding “feature tests.” Revisiting Exploring the Denote Emacs Package, you’ll notice that I’ve already introduced feature creep. Such is the life of any project, but especially a software project.

Glossary Migration

I need to consider how I’m handling abbr: and abbr-plural: links; due to the newness of this functionality I don’t have many to consider. This does require navigating the export of Hugo shortcode fragments.

I will need to migrate the Rake 📖 task that I wrote for pulling data into my Hugo 📖 data/glossary.yml file. This should be relatively straight-forward and easier by introducing the convention of a glossary directory.

Yes, I Org-Roam 📖 I could have created a custom directory and template, but I was relying on the search function as my intermediary. In hindsight, having a specific directory for Glossary is something I’m now sensing I want.

Given that I’m uncertain I want to migrate to Denote, it makes sense to move my glossary items to a glossary directory.

Blog Export

I’ll need to revisit the Extending the Ox section of my Emacs configuration. With this document, I did a called jf/export-org-to-tor to see what happened. The main thing I observed was that the Ox-Hugo front-matter I inject into this document was placed at the top of the file.

That clobbers the Denote convention of it’s front-matter at the top of the file. The fix is somewhat straight-forward; I know what to do but not yet how I’ll go about it.

There likely exists a function to detect the entirety of a Denote document’s front-matter. Use that to find where to insert the Ox-Hugo front matter.

Another thing I noticed was that I was adding an :ID: to a properties drawer. This is not necessary as Denote has the :identifier: front-matter entry. This does, however, remind me that I’ll need to consider migrating nodes.

In the last week, I recall reading a post regarding exporting Org-Roam to Denote; I’ll want to go track that down.

I found it: bitspook/notes-migrator: Migrate your notes b/w different note-taking software (only org-roam to denote supported for now)

Data Structure

One thing I love thinking about, and all too often forget to think about, is the data structure.

And Denote, by it’s conventions, has me thinking about the data structure; in part because of it’s opinions regarding front-matter. In this case there are two data structure components to consider:

  • File System Conventions
  • Document Metadata

File System Conventions

As a refresher, Denote uses the file name to encode:

  • Identifier
  • Title
  • Keywords (aka Tags)

There is further consideration about the directory structure. There is guidance on how to isolate directories (e.g. “work”, “personal”, “volunteer”) such that they are siloed from each other. See Section 3.3 “Maintain separate directories for notes” of Denote’s manual for further details on separate directories. However, directories need not be siloed. My aforementioned glossary is something I might not want siloed; after all these terms are useful for many contexts.

I do wonder how I might have three directories: glossary, employer, personal. I would want the glossary available to the employer and personal but disallow employer and personal “knowledge” of each other.

To put a name to metadata role of directory, I think of namespace or domain. Namespace feels too generic. Let’s go with domain, and lean on Webster’s 1913 definition:

The territory over which dominion or authority is exerted; the possessions of a sovereign or commonwealth, or the like. Also used figuratively.

I also like overlaying/amalgamating the definition with the Mathematical concept of domain:

The domain of a function is the set of its possible inputs, i.e., the set of input values where for which the function is defined.

With domain I have four pieces of metadata: title, identifier, keywords, and domain.

I should probably consider what are my implicit domains:

Glossary
I reference many terms.
Epigraphs
I collect passages and reference them in posts.
Blog Posts
I’ve written a lot of blog posts; should I move a note into this domain when I publish an internal note?

Do all of these warrant their own directory? Before I get carried away, I should do some preliminary exploration. As previously mentioned, the Glossary domain is a good experiment for adoption.

Document Metadata

I wrote about this quite a bit already, but will reiterate; because Denote allows writing in Org-Mode format, I have access to Org-Mode’s property syntax; though I’ll need to use a different method than I’ve previously used.

Keywords and Controlled Vocabularies

On I migrated the tags of my Blog; compressing 378 tags into 59. I checked Changelog // Take on Rules to see when I made this unceremonious switch. How to Use Tags spurred me to revisit my tagging. This migration inter-relates with my glossary migration; the glossary asserts what tags are allowed on Take on Rules.

To my knowledge, Org-Roam, does not have a concept of a controlled vocabulary for tags. And since I’m using Org-Roam as the mechanism for the initial composition of my blog, I’m butting up against tagging; namely I have to be disciplined about how I tag things.

In my quick read of the documentation, denote-infer-keywords provides the ability to limit tags to only those explicitly set in denote-known-keywords.

Further Considering and Pondering

One thing rattling around in my brain is how I’ve been using my agenda.org file. For my work at Scientist.com I use it as a TODO list and my monthly timesheet. When the month is done, I archive my timesheet.

What if I rethink things just a bit; each month get’s a journal.org file. Then I amend my org-agenda=files variable to point to the timesheet? This would mean I’m not archiving entries but instead removing them from my agenda workflow.

This is a future tangent but one that I’m pondering and considering. And none of this is really dependent on Denote; it is simply a byproduct of thinking about my organization system.

But I do like the idea of not archiving the entries. Why?

I use Git 📖 for version control of my Org-Mode files; and moving a subtree from one file to another gives me trepedations.

Conclusion

This document is about helping me think through a potential migration. And in writing this all down, I’m thinking about what it means to go through a migration.

-1:-- Migration Plan for Org-Roam Notes to Denote (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--October 02, 2022 10:09 PM

Jeremy Friesen: Exploring the Denote Emacs Package

Some Analsysis of Features

As of I use Org-Roam 📖 for writing and linking. Those notes can be glossary entries, blog posts, people, epigraphs, or just about anything text.

Org-Roam utilizes a SQLite database for storing and linking metadata. This is a feature that I consider a nice to have.

An emerging note taking utility is Denote; in this document I want to explore my usage of Org-Roam; what I’ve come to expect (e.g. consider a “must have”) and what I’ve come to appreciate (e.g. consider a “nice to have”). I then want to look at how to achieve that in Denote.

The foundational tooling that I want:

Org-Mode aware
Note taking that “understands” Org-Mode 📖; or more appropriately that Org-Mode tooling can understand.
Tags
Tag each note.
Quick filing
Insert a new note with minimal thought of where it goes.
Linking
Link to other notes.
Metadata
Add metadata that can power my epigraphs and glossary.
Search
Prompt for notes by searching title and tags and other metadata.
Export Links
When I link to a node and export, I want to export the world facing URL.

The above definitions are my “feature list”; I’ll reference those later.

A nice to have feature would be prompting for a file by more advanced searching. An example of that would be as follows:

  • Filter on the programming tag
  • Search the resulting filter set for the words Hello World

Another nice to have feature is collision detection; I don’t want two notes to have the same glossary key, nor alias. Right now I believe Org-Roam enforces unique aliases but not glossary keys.

Diving into Denote

You might be wondering why this document? There’s a two-fold reason. First, I want a place to think about what this change would mean. Second, I want to remember and be able to later evaluate all of the code I’m writing and loading into Emacs 📖 without adding it to my Emacs Configuration.

In away, this post is my sandbox exploration of the Denote package.

With this quick establishment of my note-taking requirements I’m going to explore Denote.

Why explore Denote?

First, I found myself reading the Denote documentation for pleasure. It is one well-documented package; accessible and helpful at learning not just Denote but also Emacs Lisp as well.

More importantly (maybe?) is a statement I recall from a seminar with Neil Jeffries regarding the Oxford Common File Layout (OCFL 📖). The statement was along the lines of favoring Posix, the Unix file system. It is a consistent and underpinning technology that is very likely to continue as other technologies light the stage and fade away. In other words, it has attributes that are ideal for “preservation” of digital objects. Too Long; Didn't Read 📖 Favor it over any other system of preservation.

I’ve long worked in the fields adjacent to digital preservation concerns and have felt the anguish of Fedora Commons wending a path to, from, and back to the sensible consideration of storing things on a file system.

In other words, the sensibility of Denote echoes my wariness of too many tools. However, there’s quite a bit of tooling in Org-Roam that uses SQLite to help facilitate; but the fundamentals of portability remain.

The key consideration is that Org-Roam has one more external dependency than Denote.

Reading Denote’s documentation on Portability, I see common consideration:

Notes are plain text and should remain portable. The way Denote writes file names, the front matter it includes in the note’s header, and the links it establishes must all be adequately usable with standard Unix tools. No need for a database or some specialised software.

Protesilaos Stavrou (aka Prot), the maintainer and originator of Denote, stewards many Emacs packages. A consistent attribute of those packages is fantastic documentation; both inline and of the “README” variety.

Denote’s documentation exemplifies quality documentation.

The Code I Load

When I first started, I had the minimal package declaration (use-package denote :straight t). As I explored I amended that basic declaration to the following code block.

(use-package denote
  :straight t
  :commands (denote-directory)
  :bind ("H-c a" . jf/denote-create-abbreviation)
  :custom ((denote-directory "~/git/org/denote")
	   ;; These are the minimum viable prompts for notes
	   (denote-prompts '(title keywords))
	   ;; I love org-mode format; reading ahead I'm setting this
	   (denote-file-type 'org)
	   ;; And `org-read-date' is an amazing bit of tech
	   (denote-date-prompt-denote-date-prompt-use-org-read-date t)))

(cl-defun jf/denote-create-abbreviation
    (&key
     (title (denote--title-prompt))
     (abbr (read-from-minibuffer "Abbreviation: ")))
  "Create a `denote' entry for the given TITLE and ABBR.

NOTE: At present there is no consideration for uniqueness."
  (interactive)
  (let ((template (concat "#+GLOSSARY_KEY: glossary---" abbr "\n"
			  "#+ABBR: " abbr "\n")))
    (denote title
	    '("glossary" "abbreviation")
	    'org
	    (f-join (denote-directory) "glossary")
	    nil
	    template)))

Exploration Notes

I arrived at the above code-block via the following exploration.

Customizing denote-directory

Prior to configuration my denote directory was ~/git/org/main/notes; I’m unclear why it chose this but it was a good guess as my org-roam-directory is ~/git/org and I have a capture template that writes to ~/git/org/main/.

With the configuration I’m partially sequestering my playground and will begin exploring.

Creating jf/denote-create-abbreviation

I had originally started exploring the denote-templates but chose to pursue a more explicit pathway which, based on my knowledge of Lisp, was straight forward and very quick.

There were a few turns that I took. An existing implementation for much of my tooling is that I’m assuming an Org-Mode properties drawer for the GLOSSARY_KEY and ABBR properties.

In Org-Roam the properties for the node go above the TITLE property; however by convention that is not how Denote is structured to work. I made some revisions.

I tested the above by creating a new Denote node for “Digital Humanities” with the abbreviation of “DH”.

Pause To Review Requirements and Reflect

At this point, I have verified checked off 4 of the 7 requirements. And reading ahead of the documentation I see that there are considerations for Linking and Exporting Links as well as searching.

  • Org-Mode aware
  • Tags
  • Quick filing
  • Linking
  • Metadata
  • Search
  • Export Links

At this point, what I really like is the interface to creating a note. The denote function does the magic and allows for me to pass parameters that override the default methods.

Contrast with Org-Roam, where I provide the title/text and then say what the template shouldl be.

I’m also liking the ease at which I could create a function for creating glossary entries. To do that in Org-Roam via capture is certainly doable.

The thing I need to check is how moving from the properties drawer approach I’ve used in Org-Roam varies from using keywords. Most of my interactions with properties are via Org-Mode’s API.

This sounds like the next pathway to explore.

Investigating Moving from Property Drawer to Keywords

Before I get too much further, I need to verify that I can continue to get properties from my notes.

The following function verifies that I can retrieve a property for the Denote note I made.

(cl-defun jf/denote-org-property-from-id (&key id property)
  "Given an ID and PROPERTY return it's value or nil.

Return nil when:

- is not a denote file
- ID is not an `org-mode' file
- property does not exist on the file"
  (when-let ((filename (denote-get-path-by-id id)))
    (when (string= (file-name-extension filename) "org")
      (with-current-buffer (find-file-noselect filename)
	(cadar (org-collect-keywords (list property)))))))

(message "%s" (jf/denote-org-property-from-id :id "20220930T215235"
					       :property "ABBR"))

Let’s Look at Linking

For this, I’ll need another node. I now have two nodes: 20220930T221757 and 20220930T215235. Using denote-link I create a link in 20220930T221757 to 20220930T215235.

Then in 20220930T221757 I call denote-link-backlinks. The backlink buffer is an enumeration of links. Whereas in Org-Roam the backlink and reference buffer includes the surrounding context; a nice feature but not something I consider mandatory.

And below is my must haves:

  • Org-Mode aware
  • Tags
  • Quick filing
  • Linking
  • Metadata
  • Search
  • Export Links

I need to further explain what I mean by this. I am accustomed to using my own jf/org-roam-find-node which wraps org-roam-find-node. I can filter by tags and title. With denote, I have to consider directory structure.

Let’s see about leveraging the Consult package.

(bind-key "H-f" 'jf/denote-find-file)
(defun jf/denote-find-file ()
  "Find file in `denote-directory'"
  (interactive)
  (require 'consult-projectile)
  (require 'denote)
  (consult-projectile--file (denote-directory)))

This provides me with the comparable functionality, but requires some reimaginging. However, courtesy of Section 5. The file-naming scheme of Denote documentation I can use the naming convention for tag and filename search.

Prefix the search with - for a tag and and _ for a word. This matches the functionality of what I have.

Conclusion

Reading and testing Denote, I have established feature parity in my functional needs.

What does that mean?

I am prepared to further pursue what it might mean to migrate my some 2800 Org-Roam notes to denote. I just completed a migration of my Hugo 📖 ./data/glossary.yml file to Org-Roam, so I know that it’s not an arduous process to migrate. Read about this data migration in On Storing Glossary Terms in Org Roam Nodes. I already identified the need to move from property drawers to properties that are positioned in the file after the Denote front-matter. I’d need to revisit the Ox-Hugo export process I’ve developed. As well as how I’m exporting and creating links.

I would also want to look at different directories. I like separating the different concerns (e.g. glossary, epigraph) and the ease at which I could set this up.

There is quite a bit more to consider regarding this migration. But it is an interesting (to me) exercise of consideration.

-1:-- Exploring the Denote Emacs Package (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--October 01, 2022 04:11 PM

Jeremy Friesen: On Storing Glossary Terms in Org Roam Nodes

Hacking on Org-Mode

This post started as a place for me to perform analaysis on my existing blogging ecosystem. The goal is to consolidate where I’m “storing” information. The strategy is moving elements of my blogging ecosystem into my Personal Knowledge Management (PKM 📖) system; and then exporting those back to my blog.

Writing to Think Through the Problem

At present I store my glossary in a YML file. Table 240: Take on Rules's Glossary Entity Attributes describles the attributes.

Table 240: Take on Rules’s Glossary Entity Attributes
AttributeStatusDescription
abbrOptionalA short-hand means of referencing the entity; it’s abbreviation
auto_mentionTo RemoveUsed by `glossary:autoMention` rake task
content_disclaimersOptionalConnects this entry to a content disclaimer
describedTo RemoveHave we already described this node; useful for preventing repeat querying of Wikidata
descriptionOptionalA terse explanation of the entry; often imported from Wikidata
gameOptionalIndicates the entry is a game; the value should be the entry’s key
itemidOptionalA URL that disambiguates this entry ; From https://schema.org
itemtypeOptionalFurther specification on the classification of this entry; From https://schema.org
keyRequiredHow we programatically reference this entry
mention_asOptionalWhen we add a mentionAs attribute use this value; From https://schema.org
offerOptionalThe URL from which you can buy the item; From https://schema.org
plural_abbrOptionalThe plural form of the abbreviation
plural_titleOptionalThe plural form of the title
same_asOptionalConnect the item to a more verbose description (e.g. a Wikipedia article)
tagOptionalWhen used as a tag, add a "mention" node
titleRequiredHow we linguistically reference this entry
verbose_titleOptionalA more expressive title

Considerations for Migrating Glossary into Org Ecosystem

What would it look like to move the glossary into my Org ecosystem?

Let’s consider the following:

  • What would be the benefit?
  • What would be the storage strategy?
  • How to map the YAML entry to the Node’s data structure?
  • What are the ways in which Take on Rules references the glossary?

What Would be the Benefit?

The primary benefit that I see is in consolidation. Right now the benefits of the glossary are only available in my blogging ecosystem. And the “capture” process for those entries is outside of my normal capture process.

Further, in consolidation there is the process of thinking through the “problem” and designing a solution. The very thought exercise itself is enriching.

What Would Be the Storage Strategy?

I see two options: ./glossary or ./refs. The advantages of ./glossary is crisp demarkation. However I envision “promotion” or “drift” of refs into glossary items.

Decision
Store nodes in ./refs and add tag :glossary:

How to map the YAML entry to the Node’s data structure?

There is an analogy between my Epigraphs and my Glossary setup. An Epigraph has properties and it’s textual content (see Even if you decide never to write a single…).

In the case of a Glossary entry, the body of the node would be my additional notes. The node’s #+title: line would be it’s title property.

What Are the Ways in Which Take on Rules References the Glossary?

The following short-codes, part of the TakeOnRules Hugo Theme reference the glossary data:

themes/hugo-tufte/layouts/shortcodes/blockquote.html
when quoting a game, I add a purchase link to the citation.
themes/hugo-tufte/layouts/partials/header/contentDisclaimers.html
when a post has a glossary tag that has a content disclaimer, I look that up.
themes/hugo-tufte/layouts/shortcodes/glossary.html
this is where the major heavy liftin occurs; a glossary entry can be an abbreviation, mention, link, etc.

themes/hugo-tufte/layouts/_default/list.html :

themes/hugo-tufte/layouts/_default/single.html :

My assumption is that I would generate the ./data/glossary.yml file from my Org ecosystem.

To continue to leverage the ./data/glossary.yml as-is I would want to have means in my Org ecosystem to declare each of these.

Design

There are two major design considerations:

  • Registering New Link Types in Org-Mode
  • Data Migration

I’ve been working on this for a few days. During that time, and separate from this work, Charanjit Singh wrote Extending org-mode to handle youtube links.

Org-Mode 📖 has different link handlers. One example is the roam: scheme; a scheme added by Org-Roam 📖.

The org-link-set-parameters exposes a means of registering new schemes.

I have two “links” that I want to add: abbr: and abbr-plural:. The goal is to use my glossary to “link” to an entry that has an abbreviation and then at the point of export expand that abbreviation.

Reading the org-link-parameters documentation there are functions I want to set for both abbr: and abbr-plural::

:complete
What are the possible entries I have that meet the criteria? (e.g. have an abbreviation?)
:export
When I export this link, what should logic should I use? (e.g. for HTML 📖, I should use an ABBR-tag, for other things the title (abbreviation) is perhaps best)
:follow
When I “click” on the link in my Org-Mode document, how should it resolve? Where should it go?

In my blog I’ve used the DFN-tag; and that could be useful. But there’s always a matter of flow regarding a term and it’s definition.

Data Migration

I want to export my ./data/glossary.yml to Org-Mode. There are some glossary entries that already have nodes in Org-Roam; for those I need to reconcile and adjust. For the others, I need to create a node for each glossary entry.

Once that is complete, I will stop adding and editing entries in ./data/glossary.yml; instead I will export them from my org repository to that file. An ideal test is that when I complete the round trip, my ./data/glossary.yml is unchanged.

I already do this for Epigraphs, so follow a similar “public” api process.

Conclusion

After a bit of time and exploration, I have enriched my understanding of the Org-Mode / Org-Roam ecosystem and further worked towards treating my blog as a “consumer” of my PKM system.

I’ve also solidified my love of cl-defun, &key parameters, and providing defaults. This helps me repurpose functions and think about their interactions. It’s a pattern I’ve also applied in my Ruby code.

An example perhaps? The following is a method signature: (cl-defun jf/org-roam-external-url-for (&key node (scheme "http"))

To call that method I write the following: (jf/org-roam-external-url-for :node the_node); where the_node is an Org-Roam node. By default I’m looking for URLs that start with the http scheme.

Instead of burying the default http in the method definition, I parameterize it and specify the default.

All of this is in service to sharpening my tools.

-1:-- On Storing Glossary Terms in Org Roam Nodes (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--September 27, 2022 01:11 AM

Jeremy Friesen: Org Mode Capture Templates and Time Tracking

Sharing My Workflow and Tooling

In this post I’ll share my workflow and the Emacs functions I use to facilitate my workflow. One highlight is better understanding how to use org-capture\’s (file+function "filename" function-finding-location) target element.

Context

One of my administrative tasks for my role at Software Services by Scientist.com is time tracking. It’s been over a decade since I last tracked my working hours. In my role I’m both coding and helping get team members unstuck on their tasks. This means on a given day, I jump between 5 to 7 projects. I find it invigorating helping get folks unstuck; either listening to their approach or digging in and pulling out a myriad of debugging and triage heuristics I’ve developed. To help me with keeping track of all of my hours and I work, I have begun leveraging even more of Emacs’s Org-Mode; a framework and toolkit for note taking and time tracking (and so much more).

My Current Workflow

At the start of my day, I review my existing todo items. This helps me remember where to start.

As I work on a todo item, I record time and take notes; which involves links and capturing further general documentation. Those notes will sometimes turn into blog posts or playbook articles. As I start a new project:

  • I start tracking my time.
  • Write a bit about what I’m working on.
  • And start taking notes.
  • For tasks that I don’t complete, I mark as todo.

As I wrap up a project’s task I go back to my todo items. The org-agenda function provides a menu of options to view my time and todo items. See the documentation At the end of the month I then go through my projects and record that time. I do all of this in my org-mode agenda file.

Code Supporting My Workflow

Before I started down this path I spent a month exploring, noting, and adjusting my workflow. As the month closed, I started to see the pattern I could use to extend my existing toolkit to better help my emerging workflow.

This section goes into the technical implementation.

Here’s my org-capture-templates. There are two of them:

project
The client’s project that I’m working on.
task
The task within a project.

For those reading along, here’s the documentation for org-mode’s the [capture template documentation](https://orgmode.org/manual/Template-elements.html).

Due to a present implementation constraint, which I’ll get to later, I need to first create the project’s parent day node. That is done via the file+olp+datetree directive; But I’m getting a bit ahead of myself.

(setq org-capture-templates
      '(;; Needed for the first project of the day; to ensure the datetree is
	;; properly generated.
	("p" "Project"
	 entry (file+olp+datetree jf/primary-agenda-filename-for-machine)
	 "* %(jf/org-mode-project-prompt) :project:\n\n%?"
	 :empty-lines-before 1
	 :empty-lines-after 1)
	("t" "Task"
	 ;; I tried this as a node, but that created headaches.  Instead I'm
	 ;; making the assumption about project/task depth.
	 plain (file+function jf/primary-agenda-filename-for-machine jf/org-mode-find-project-node)
	 ;; The five ***** is due to the assumptive depth of the projects and tasks.
	 "***** TODO %? :task:\n\n"
	 :empty-lines-before 1
	 :empty-lines-after 1)
	))

Anywhere in Emacs I can call org-capture (e.g. C-c c in Emacs dialect).

Begin Capturing Notes for the Project

The capture for the project positions the content in the following headline tree:

  • Year (e.g. 2022)
    • Month (e.g. 2022-09 September)
      • Day (e.g. 2022-09-03 Friday)
        • Project

The capture template for the project is (e.g. * %(jf/org-mode-project-prompt) :project:\n\n%?).

For the Project capture template, this:

  • creates a headline
  • prompts for the project
  • tags the node as a :project:
  • positions the cursor to begin note taking.

The following function prompts me to select an existing project or allows me to enter a new one.

(defun jf/org-mode-project-prompt ()
    "Prompt for project based on existing projects in agenda file.

Note: I tried this as interactive, but the capture templates
insist that it should not be interactive."
    (completing-read
     "Project: "
	 (sort
     (-distinct
      (org-map-entries
       (lambda ()
	 ;; Get the entry's title
	 (org-element-property :title (org-element-at-point)))
       ;; By convention project’s are:
       ;; - a level 4 headline
       ;; - tagged with :project:
       "+LEVEL=4+project"
       ;; Look within all agenda files
       'agenda))
     #'string<)))

When I started I thought I would need to create a local variable for projects. But I use org-map-entries to dynamically query the document for existing projects.

I also spent some time on the prompting function; in part because I thought it needed to be interactive. It does not.

Begin “Capturing” Notes for the Task

The “Task” capture template uses the file+function directive to find where in the document to insert the task.

The first parameter (e.g. jf/primary-agenda-filename-for-machine) specifies the agenda file for my machine. Those machines are work and personal; each with their own todo lists. The second parameter (e.g. jf/org-mode-find-project-node) is defined below; it finds and positions the cursor at the end of the given project within the give date.

;; Inspiration from https://gist.github.com/webbj74/0ab881ed0ce61153a82e
(cl-defun jf/org-mode-find-project-node (&key
					   (project (jf/org-mode-project-prompt))
					   ;; The `file+olp+datetree` directive creates
					   ;; a headline like “2022-09-03 Saturday”.
					   (within_headline (format-time-string "%Y-%m-%d %A")))
    "Find and position the cursor at the end of
the given PROJECT WITHIN_HEADLINE."
    ;; Ensure we’re using the right agenda file.
    (with-current-buffer (find-file-noselect jf/primary-agenda-filename-for-machine)
      (let ((existing-position (org-element-map
				   (org-element-parse-buffer)
				   'headline
				 ;; Finds the end position of:
				 ;; - a level 4 headline
				 ;; - that is tagged as a :project:
				 ;; - is titled as the given project
				 ;; - and is within the given headline
				 (lambda (hl)
				   (and (=(org-element-property :level hl) 4)
					;; I can't use the :title attribute as it is a
					;; more complicated structure; this gets me
					;; the raw string.
					(string= project (plist-get (cadr hl) :raw-value))
					(member "project" (org-element-property :tags hl))
					;; The element must have an ancestor with a headline of today
					(string= within_headline
						 (plist-get
						  ;; I want the raw title, no styling nor tags
						  (cadr (car (org-element-lineage hl))) :raw-value))
					(org-element-property :end hl)))
				 nil t)))
	(if existing-position
	    ;; Go to the existing position for this project
	    (goto-char existing-position)
	  (progn
	    ;; Go to the end of the file and append the project to the end
	    (end-of-buffer)
	    (insert (concat "\n**** " project " :project:\n\n")))))))

Current Implementation Constraint

My workflow does not need the “Project” capture. However the “Task” capture needs the headline structure that the “Project” capture creates. Future work that I could do would be for the “Task” capture to create the correct headline(s). But that’s a once a day inconvenience.

My Daily Task Sheet

Last the org-clock-report function provides a plain text tabular breakdown of my work days. Below is an anonymized example:

#+BEGIN: clocktable :scope subtree :maxlevel 5  :tcolumns 4
#+CAPTION: Clock summary at [2022-09-03 Sat 10:12]
| Headline                                           | Time    |       |      |      |
|----------------------------------------------------+---------+-------+------+------|
| *Total time*                                       | *14:30* |       |      |      |
|----------------------------------------------------+---------+-------+------+------|
| \_  2022-09 September                              |         | 14:30 |      |      |
| \_    2022-09-01 Thursday                          |         |       | 7:45 |      |
| \_      Client 1                                   |         |       |      | 0:30 |
| \_        Merge and Backport...                    |         |       |      | 0:30 |
| \_      Client 2                                   |         |       |      | 2:15 |
| \_        Get Bitnami SOLR Blocking Done           |         |       |      | 2:15 |
| \_      Learning Time                              |         |       |      | 1:30 |
| \_        Adjusting Time Tracking Automation...    |         |       |      | 0:30 |
| \_        Submit Proposal for Responsible...       |         |       |      | 0:30 |
| \_        Show and Tell                            |         |       |      | 0:30 |
| \_      Client 3                                   |         |       |      | 1:45 |
| \_        Pairing with A regarding Workflows       |         |       |      | 1:45 |
| \_      Client 4                                   |         |       |      | 1:15 |
| \_        Pairing on #138                          |         |       |      | 1:00 |
| \_        Reviewing...                             |         |       |      | 0:15 |
| \_      Client 5                                   |         |       |      | 0:30 |
| \_        Pairing with B on Collection Slugs       |         |       |      | 0:30 |
| \_    2022-09-02 Friday                            |         |       | 6:45 |      |
| \_      Client 6                                   |         |       |      | 0:15 |
| \_        Pairing with C regarding rebase...       |         |       |      | 0:15 |
| \_      Learning Time                              |         |       |      | 0:15 |
| \_        Writing About Emacs and Org-Mode Time... |         |       |      | 0:15 |
| \_      Client 1                                   |         |       |      | 2:15 |
| \_        Working on troubleshooting upstream...   |         |       |      | 0:30 |
| \_        Work on Documenting Hyrax’s IIIF...      |         |       |      | 1:45 |
| \_      Samvera                                    |         |       |      | 0:15 |
| \_        Reviewing PR for a Hyrax app without...  |         |       |      | 0:15 |
| \_      Client 2                                   |         |       |      | 1:30 |
| \_        Working on getting SOLR up and...        |         |       |      | 1:30 |
| \_      Client 7                                   |         |       |      | 1:15 |
| \_        Client 7 Upgrade Estimate                |         |       |      | 1:15 |
| \_      Client 5                                   |         |       |      | 1:00 |
| \_        Universal Viewer Overview                |         |       |      | 0:45 |
| \_        Working with D on Collections            |         |       |      | 0:15 |
#+END:

In the actual time sheet each of those lines link to the corresponding headline. The provides another way to navigate.

Conclusion

I never quite realized that I would appreciate time tracking. It helps me ensure that I’m not working more hours than I should. At other places, I’d work more hours. Here the time sheet helps set clear boundaries.

This workflow also helps me recover from context shifting. I want to help people get unstuck, but jumping in and out of that context does come with a cognitive cost. The underlying technical workflow provides the ritual/habit for re-orienting to what comes next.

As I mentioned earlier, my agenda file becomes a source for knowledge sharing; either with my future self or as a blog post. This article began as a quick note in my agenda file. And in that agenda file I’ve linked to this article.

Now to write a function to generate my daily stand-up “what did I do”; it should be rather straightforward based on my well structured time sheet and notes.

And as always, you can look to my dotemacs repository for even more regarding my Emacs configuration.

-1:-- Org Mode Capture Templates and Time Tracking (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--September 06, 2022 01:21 PM

Jeremy Friesen: On Sharpening Your Tools

Using a Tool and Knowing Its Uses Are Not the Same

Save some time to dream
Save some time for yourself
Don't let your time slip away
Or be stolen by somebody else
John Mellencamp, Save Some Time to Dream

Confession time, as a child, I have a distinct memory of using one of my father’s chisels as a screw driver. I suppose I should apologize for that.

I received a lovely email that included the following question:

Is it common for programmers to spend a bit of their working hours fixing/sharpening their tools?

As I wrote up my response, I asked for permission to reference their question. The sender obliged, so here’s what I wrote (with some editing to reflect a broader audience).

The Value of Fixing/Sharpening Your Tools

This is a great question and I have two trains of thought.

First, I don’t know if you’ve heard of DEV but this kind of question is one that the community loves to interact with. I work for the Forem, the maintainers of the software that powers DEV; so I’m a bit biased on sending people there.

Second, let’s look and my specific situation and understanding.

It is my assumption that in the process of using my tools, I explore those tools for further utility or “productivity” gains. This builds on the idea that I am looking for outcomes not time in seat/at keyboard. And please know that one outcome I consider absolutely enjoyable is the puttering of time simply playing with a computer. Much like idly whittling wood with a knife is quite enjoyable; and while the tangible outcome is a pointy stick, the intangible is a sense of wonder. A case in point, I spent about 2 hours writing an emacs lisp function that helped me tidy up the the blend of our pull request template and the commit message). There are (or were?) a few outcomes of taking time to do that:

First, the solution saves me about 20 seconds of time each pull request. I’ve used that function about 150 times since writing it, so I’m at 50 minutes saved with a 2 hour spend. In addition, those 20 seconds saved also included micro-decisions.

By automating a bit of those tasks, I’ve reduced one location that might cause decision fatigue.

Second, I now have a mental pathway of how to do this or what might be possible; which helped me quickly write a script to facilitate writing end of week reports (10 minutes or so?).

I now use that new function about 3 times a week. I likely saves me 30 seconds of copy paste plus the context switching of multiple tools.

This pattern repeats itself in other tasks.

I have long mapped Cmd \+ . to toggle between spec/test and file. That alone has helped me always think about my test as well as the predictable file system structure necessary to sustain that pathway.

I have watched other developers open their project navigation tree, and click through folders to get to the related test. That consumes decision-making resources.

I recently fiddled with installing tmr.el, a timer package for Emacs (Emacs 📖). Why? Because I can now easily set a timer within Emacs. I don’t need to grab my phone (which the vary act of doing already breaks my mental thinking).

Do I anticipate using tmr.el much? No, because it’s not often that I want a timer while I’m at my computer. But it’s there. And I practiced setting it.

A final case of working with Emacs is work I did to help with my org-roam contexts. I can select a set of tags (or a named group of tags) to auto-filter my Org-Roam commands: capture, find, insert, or Completion at Point Function (CaPF 📖).

What this has meant is I can easily write work notes and play notes in the same directory. I can then both interconnect those notes but also help me not “accidentally” inter-connect them. This helps me remove a decision-making point.

It has also helped me begin moving my blog posts from stand-alone Hugo files into my Knowledge Management System (PKM 📖) system.

And I try to do all of this “exploration” of my editor when I’m working on a well understood to me problem.

When I’m trying to reason through something more complicated or vague, I avoid trying to also dive into my text editor. But I might make a quick note to tell myself “Hey you did this thing…maybe spend some time thinking about it after you’re done.”

I hope this provides some context on why you personally would want to do it, and how it’s not about asking for permission but to instead have that be part of your software development process.

Ending with a Question

Now I’m wondering, what have you learned in sharpening your tools?

update

A reader reminded me of the XKCD #1205: Is It Worth the Time?.

That comic strip provides some guidelines on how long it will take to see a return on time spent for a task.

My hope, in my post, is to allude to the compounding nature of working on your toolset. To not simply look at the how much time will I save but to leaves space to answer the following question:

While I’m figuring out and telling the computer how to save me time on this task, what else am I learning about myself and my toolset?

In other words, take time to play (as in the developmental process by which we learn) with your tools.

-1:-- On Sharpening Your Tools (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--April 29, 2022 08:20 PM

Jeremy Friesen: Org-Mode, Git Logs, and Org-Babel for Running Reports

Feeding the Personal Knowledge Management System

In Using a File as a Template in Emacs, I wrote about the end of week reports that my supervisor tasked me with writing. I want to expand a bit on one of the scripts I uses to help me fill out that report.

The Script

The following script is one of the tools I use to help me write my end of week report. It only considers the Forem code base.

cd ~/git/forem ;
echo "Authored PRs Merged Since 2022-03-25\n"
git log --since=2022-03-25 \
  --format="%h — %s %an, (%cs) %d" \
  --author="(Suzanne A|Jeremy F|Arit A|Dwight S|Anna B)"\
  | rg "— ([^\(]+) \(\#(\d+)\) ([^,]+)," --only-matching -r '- $1 :: forem/forem#$2 $1' \
  | sort
echo "\nCo-authored PRs Merged Since 2022-03-25\n"
git log --since=2022-03-25 \
  --grep="Co-authored-by: (Suzanne A|Jeremy F|Arit A|Dwight S|Anna B)" \
  --format="%(trailers:key=Co-authored-by,separator=%x2C,valueonly=true) :: %s" \
  | rg "^([^<]+) <.*> :: ([^\(]+) \(\#(\d+)\)" --only-matching -r '- $1 :: forem/forem#$3 $2' \
  | sort

In the above script there are two sections:

  • The first section are the commits authored by members of the Content Experience Pod.
  • The second section are the commits in which pod members contributed one or more commits to the PR but were not the initiating author; Git registers these as Co-authors.

In other words, the script shows what code my team helped ship for the week.

Where to File Away that Script?

For a week or two I was running a simpler version of the above script; I would search through my shell command history, find the one that looked right, and adjust the date.

That worked but I’d prefer to not rely on that workflow. I added the script to my Content Experience Pod’s Org-Roam node; a document that is part of myKnowledge Management System (PKM 📖) system.

Here’s what it looks like in that document:

#+Begin_src sh :results output :cmdline (org-read-date)
  cd ~/git/forem ;
  echo "Authored PRs Merged Since $1\n"
  git log --since=$1 \
    --format="%h — %s %an, (%cs) %d" \
    --author="(Suzanne A|Jeremy F|Arit A|Dwight S|Anna B)"\
    | rg "— ([^\(]+) \(\#(\d+)\) ([^,]+)," --only-matching -r '- $3 :: forem/forem#$2 $1' \
    | sort
  echo "\nCo-authored PRs Merged Since $1\n"
  git log --since=$1 \
    --grep="Co-authored-by: (Suzanne A|Jeremy F|Arit A|Dwight S|Anna B)" \
    --format="%(trailers:key=Co-authored-by,separator=%x2C,valueonly=true) :: %s" \
    | rg "^([^<]+) <.*> :: ([^\(]+) \(\#(\d+)\)" --only-matching -r '- $1 :: forem/forem#$3 $2' \
    | sort
#+end_src

It’s a little different, because I’ve written it using Org-Mode’s Babel syntax.

The first line (e.g. #+Begin_src sh :results output :cmdline (org-read-date)) declares:

  • A code-block with sh syntax
  • To write results as raw output
  • To pass the results of the (org-read-date) function as $1 to the script

The remaining lines are almost verbatim of what I previously wrote; except instead of the “hard-coded” date of I’m using the results of the org-read-date function.

To run the report, I set point (e.g. the cursor) in that code block, type C-c C-c and Emacs 📖 evaluates the Babel block.

First it calls the org-read-date function, prompting me to select a date. Then it runs the shell command. And outputs the results just below the Babel block.

From there, I can see one aspect of the work my team has done for the week.

Conclusion

Prior to adopting Emacs, and Org-Mode specifically, I would’ve floundered on where to put this. It would’ve remained in my shell history.

But now that I’ve associated this with a group of people, written it in a less ephemeral file, and written about it, I will be more likely to remember both it’s existence and what it did.

I learned about Git 📖’s commit trailers and how to find the Co-authors of a commit. Which helps make visible the work that folks do to help another person get their pull request merged.

I also learned a bit more about passing arguments from Babel into the script it’s executing.

And last, because I wrote this blog post in my PKM system, I have a reference from the published blog post, to the Content Experience Pod.

In other words, I’m doing my best to create breadcrumbs to help me find the particulars of a script I wrote.

-1:-- Org-Mode, Git Logs, and Org-Babel for Running Reports (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--April 02, 2022 04:00 PM

Jeremy Friesen: Using a File as a Template in Emacs

Minor Automation to Facilitate End of Week Reporting

At Forem, one of my responsibilities is writing up an end of week status report for the projects assigned to my team. Sometimes I delegate that responsibility (if there’s someone with more information for the week’s update).

I’ve found that I enjoy writing these reports. I spend about thirty minutes per project writing up the report. During that time I gather what’ve we done, what we’re planning to do next week, and write up any risks to the project.

Earlier this week, Allison, our head of engineering, provided an adjusted template to help facilitate writing consistent reports for tracking issues.

I figured I’d go ahead and automate Emacs 📖 to help me use that template.

Forem End of Week Status Update

The following emacs-lisp creates a buffer, from an existing template, to help kick off writing my end of week status reports.

(defvar jf/forem-eow-template
  "~/git/forem-internal-eng/.github/epic-progress-update.md"
  "The location of the template to use for end of week reporting.")

(cl-defun jf/forem-prepare-end-of-week-status-update (&key (template jf/forem-eow-template))
  "Create a buffer for writing up an Engineering End of Week Status Update.

TODO: Consider pulling down the latest version of that template."
  (interactive)
  (let* ((body (with-temp-buffer
		 (insert-file-contents template)
		 (buffer-string)))
	 (eow-buffer (get-buffer-create "*Forem EoW Update*")))
    (switch-to-buffer eow-buffer)
    (erase-buffer)
    (markdown-mode)
    (hammerspoon-edit-minor-mode)
    (insert body)
    (beginning-of-buffer)
    (kill-line)
    (insert (concat "## " (format-time-string "%Y-%m-%d")))))

Details

The above code:

  • copies the contents of the template
  • creates a new buffer titled *Forem EoW Update*
  • sets it as Markdown type content
  • enables Hammerspoon (see below)
  • pastes the contents into the new buffer
  • sets the first line to today’s date

I use Hammerspoon and the editWithEmacs.spoon to help me use Emacs for editing Emacs text areas. I wrote about that in Send Anything in OS X to Emacs for Editing. .

Conclusion

This little bit of automation ensures that I’m using the consistent template and am writing using my favorite computer tool. It’s a quick bit of automation, but one that I need to leverage at least once a week for the foreseeable future.

-1:-- Using a File as a Template in Emacs (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 25, 2022 05:28 PM

Jeremy Friesen: Further Into Emacs with Pcase, Backquotes, and Commas

The Continuing Adventures of Writing to Learn

This post is a callback to Alex Schroeder: 2021-09-17 Writing to learn; to remember that writing is one method of explaining something. In this case, I’m explaining to myself what I’ve learned.

I submitted the following issue for org-roam: Allow for `org-roam-buffer`’s Backlinks section to be unique per source. On I submitted a patch to address the issue: Adding unique option fro org-roam-backlinks-section.

While chatting with Jethro Org Roam’s maintainer he suggested using a pcase construct. I have read the pcase documentation and struggled to sift through it. It’s right on the boundary of my comprehension. So I proceeded with my pull request.

Later, I submitted a proposal for a customization, and Jethro explained that the pcase construct would likely be cleaner and more generalizable. He then wrote up that change and pinged me. Thank you Jethro, now I have a pcase use case that I understand what we’re doing, which will help me move pcase further into my area of comprehension.

To learn, I’m going to write about the change that Jethro put forward:

(dolist (section-fn org-roam-mode-section-functions)
  (pcase section-fn
    ((pred functionp)
     (funcall section-fn org-roam-buffer-current-node))
    (`(,fn . ,args)
     (apply fn (cons org-roam-buffer-current-node args)))
    (_
     (user-error "Invalid `org-roam-mode-section-functions specification.'")))))

As I’m reading through the code, I also have on hand the pcase manual section and the backquote manual section.

Line 1: This iterates over the org-roam-mode-section-functions list. Each element of the list is section-fn. The element is “passed” to the “anonymous function” that is lines 2 through 8 (e.g. the “body” of the dolist).

Line 2: The pcase expression we’re evaluating is the section-fn. Reading the docstring for pcase, it doesn’t say mention it explicitly, but the verbose name for pcase could be patterning-matching-case-statement.

Line 3: This is the first pattern that we check. This line answers the question: Is the section-fn a function?

Line 4: When section-fn is a function, call that function passing the org-roam-buffer-current-node as the only argument.

Line 5: This is the line that breaks me. What I do know is that when section-fn is (org-roam-backlinks-section :unique t) then this is a match. But

Line 6: Call the fn (which is declared in line 5?) with the org-roam-buffer-current-node and the args. Okay, this is breaking my brain a bit.

Line 7 and 8: The fallback is to raise a user-error.

That Which Breaks Me

On a cursory read, line 5 and 6 confound me. My mind wonders what is fn and args? How do they become the “variables” of line 6?

While thrashing against this, I started building up some search terms: “pcase emacs backquote”. Which lead me to Backquote-Style Patterns. Jackpot!

Backquote-style patterns are a powerful set of pcase pattern extensions (created using pcase-defmacro) that make it easy to match expval against specifications of its structure.

Of course, it’s in the manual. And reading further, I see the following:

The first three clauses use backquote-style patterns. `(add ,x ,y) is a pattern that checks that form is a three-element list starting with the literal symbol add, then extracts the second and third elements and binds them to symbols x and y, respectively.

The commas are used to “extract” elements of the section-fn and allow them to be used later on. I’m trying to connect this to my extensive Ruby (Ruby 📖) experience, and I’m struggling to do so.

I’ve used case statements before, but I hadn’t considered how I might use comparison statement as the thing that also “declares” the variables for evaluation.

Thank You

So thank you Jethro for taking the time to refine the org-roam code. This has helped me further develop my understanding of Emacs.

-1:-- Further Into Emacs with Pcase, Backquotes, and Commas (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--March 13, 2022 03:52 PM

Jeremy Friesen: Note Taking with Org Roam and Transclusion

Ever Improving my Personal Note Taking Process

This past week, I’ve been migrating my blogging workflow towards an Org-Mode-first workflow. By that I mean I’m writing my blog posts using Org-Mode 📖 and exporting the content to Hugo for building Take on Rules.

Why This Added Layer of Effort?

The short answer to “why this added layer of effort” is because of Org-Roam and Org-Transclusion. But those are technical implementations. First I want to talk about the functional issues I’ve encountered.

I write a lot of notes. For work, I keep a daily list of tasks I’ve worked on. I also write meeting notes

And I write notes to think through a technical approach. For all of that, I use Org-Mode. For commit messages and interactions with GitHub, I use Markdown.

For play, I write session reports, poetry, thoughts about games, reviews, and so on. Up until recently, I’ve written most of that with Markdown.

In other words, I had two different formats for my non-programming writing. That alone isn’t a reason to change; and is one reason I previously had not.

Yet this bifurcation sat as a mental irritant, even though it was not quite a problem to solve. A year ago someone told me that at some point I’d be migrating to Org-Mode for blogging. So credit to them for seeing that future. I chose to sit with this mental friction, to better understand the problem I was experiencing.

What Changed?

A slow moving confluence of moments brought about this change.

When I was facilitating the New Vistas in the Thel Sector campaign, I wrote notes using Org-Mode and Org-Roam and exported my notes to Markdown. At the time, I would then finesse the export. This involved scrubbing links to “private” notes. My Game Master (GM 📖) notes if you will. I was also new to Emacs and not yet comfortable with emacs-lisp.

As that campaign spun down, I started writing functions to help me compose Markdown blog posts. These functions lean heavily on yasnippet and later custom emacs-lisp. In all of this, I wasn’t ready to address the mixture of “public” and “private” notes paired with the idea of yet another content migration. I have migrated this blog’s page generation from Wordpress

Fast forward to , and I’m playing in two Burning Wheel Gold (BWG 📖) games. I’m facilitating the Mistimed Scroll campaign. I am about 5 session reports behind on this campaign . And I’m playing in Burning Locusts. In both campaigns, one of the fellow players is also an Emacs enthusiast.

When they shared their DOT notation graph representating the relationships in Graphviz, I followed with Using PlantUML to Model RPG Relationship Maps. They then wrote Burning Plants.

That exchange was another brain-worm that nudged me revisit my blogging process.

Why? Because Org-Roam’s Org-Roam UI can generate an interactive graph based on nodes. And more importantly, I had found and was playing games with a fellow gamer that was interested in Emacs, relationship graphs, note taking, and many other shared interestes. The kind of gamer who I’d love to meet for coffee and just talk about games and theory.

Before I go further, I want to briefly work through some graph terms.

Node
a chunk of descriptive information
Edge
a reference from one Node to another Node

In Org-Roam, when I create Node A and add a link to Node B, that creates an edge A -> B. In Org-Roam, Node A references Node B and Node B has a back link to Node A.

Org-Roam also exposes the concept of :ROAM_REFS:. Let’s say I add Node C to my notes. For Node C lets set its :ROAM_REFS: to include “https://orgmode.org”. Now, anytime I link to “https://orgmode.org” in my notes, Node C will have a back link to that reference.

In other words, :ROAM_REFS: let me create proxies for external concepts.

As I learned about that, I started exporting my blog’s posts to my Org-Mode directory. The original reason for export into Org-Mode was because I have also been writing more technical blog posts that I wanted to reference in my private note-taking; a confluence of labyrinthine moments.

But, I was treating my blog as the primary source of knowledge. And that runs contrary to the actual model. My thoughts are private, and in speaking them, they become public. But I digress a bit.

Back to taking Session Notes

With my blog now “imported” into my Org Mode ecosystem. Not migrated just imported via some Ruby scripts and Pandoc antics. Critical in this import is that I would generate a node ID and a :ROAM_REF: entry. Thus creating the connection between public URL and private node identity. I started thinking about the directional flow of information.

This is when I again revisited Ox-Hugo. I was looking to scratch the itch of resolving the directional flow of knowledge, and the Do I need to re-write my whole blog in Org? page gave me confidence to spend a bit of time exploring.

I started with a Literate Programming approach and wrote my takeonrules.org export functions. I also mentally framed this whole thing as an experiment; something I would test and observe and rollback if necessary.

The main concept being that I wanted to correct the flow of information (e.g., private to public). When exporting a node, I did not want to export links to private nodes. It is okay to export the text but I don’t want to export broken links.

And my experiment worked.

But Wait, There’s More

As I solve one problem, I become aware of more opportunities that arise with the new state. And I owe you, dear reader, information about Org-Transclusion.

Once I had the export working, I started looking at the graphical structure of my notes in Org-Roam UI. And as structured, each game session node (e.g. the node I publish to my blog) had lots of edges. After all, I was writing all of these notes in one node. My custom export process assumes that I’m exporting one file which has one Org-Roam node; an implementation detail that has thus far been adequate

Which node has the reference impacts the back links. To make it concrete; my session notes incorporate Non-Player Characters (NPCs 📖), and while back links from the NPC to their sessions is nice, I’m often more interested in the scenes in which they were present.

Around this time, I also got the wild idea of “How might I, a player in Burning Locusts share my notes with the game facilitator, such that they could overlay their notes on my notes. I spent a bit of time thinking through that in my Burning Locusts Campaign Data repository. You can also see a snapshot of the campaign data.

Which brings me to Org-Transclusion. I started reworking this in my to be published Burning Locusts: Session 7 notes. At the time of writing this I linked to my internal notes, but you dear reader, will not have such a link until I both re-export this post and publish the session notes.

What I’ve now chosen to do is to create a node for each scene. And transclude those nodes into my session report. In a way, my session report is an index of scenes. Here’s what that session report looks like:

:PROPERTIES:
:ID:       4E332C1F-57FA-47D3-B303-A4B21AF3BA3B
:SESSION_REPORT_DATE: 2022-02-16
:SESSION_REPORT_GAME: BURNING-WHEEL-GOLD
:SESSION_REPORT_LOCATION: via Discord and Roll20
:END:

#+title: Burning Locusts: Session 7
#+SUBTITLE: Arson, Ambush, and Art Sales
#+FILETAGS: :session-report:burning-locusts:rpg:burning-wheel:

* TODO More Happenings at Adriano’s Party

#+transclude: [[file:20220225---adriano-faraldo-s-party.org::*Session 7][Session 7]] :only-contents


* TODO Aftermath of the Ambush at Adriano’s

#+transclude: [[id:1226FDD8-E7D3-4AF1-9958-5DC0ABE721FF][Aftermath of the Ambush at Adriano’s]]


* DONE Background Events Resolved After Adriano’s Party

#+transclude: [[id:6AC158C5-2279-4A80-916F-E087F5B6FF2D][Background Events Resolved After Adriano’s Party]]


* DONE Frederico Meets with the City Clerk

#+transclude: [[id:8761371C-B4C2-4E06-8C13-135BB0780382][Frederico Meets with the City Clerk]]


* DONE Antonius and Maccio Have a Conversation Regarding the Arsonist

#+transclude: [[id:7DABFA0F-5200-46E0-A494-F9EBFD23CBAD][Antonius and Maccio Have a Conversation Regarding the Arsonist]]


* DONE Frederico and Antonius Seek a Fence

#+transclude:  [[id:0BEFCA44-8DA4-4167-A727-07F676F6EBD1][Frederico and Antonius Seek a Fence]]

* TODO Closing Scene

I have minimal memory of this, as it was very much a denouement.

For transclusions to “register” as back links, I removed (keyword "transclude") from the org-roam-db-extra-links-exclude-keys variable. I submitted an issue to org-transclusion describing the behavior without this adjustment

You’ll also note that I repeat the header with the label of the transclusion. I wouldn’t need to do this, if I moved the header into the file. But I prefer this method.

Conclusion

Riffing on Clarke, and as I’ve said before, “Any sufficiently advanced hobby is indistinguishable from work.”

I have a lot of tooling for helping me write Markdown blog posts, and nothing that I’ve done invalidates that. Instead I’ve extended my workflow to allow me to now better take and share session notes. If I’ve learned anything in my 2 years with Emacs 📖, all aspects of my computer life benefit when I experiment with writing and knowledge management.

For example, when I’m writing my daily work activity log and am working with someone on that activity, I add a reference to those coworkers. Yes, there’s a node for each person I interact with at work . This way, if I need to recall information, I have the back link available as a tool to assist on that recollection.

Postscript

As I finished writing this post, I realized that another catalyst in this change was adopting a Literate Programming approach. I spent time moving my Emacs configuration to use org-babel-tangle, which allows for a mixture of prose and code. Which is not to confuse the prose with code comments.

It turns out that taking the time to write through an observed problem with prose tools while also having access to coding context all in the same file helps me better think through these problems.

-1:-- Note Taking with Org Roam and Transclusion (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--February 26, 2022 08:26 PM

Jeremy Friesen: Org Roam, Emacs, and Ever Refining the Note Taking Process

Always Be Refining Your Config

I want to write about my third iteration on an org-roam. It’s goal is to address use-cases that I’ve encountered while moving more of my note-taking with org-roam.

One use-case is when I’m running or playing in an Role Playing Game (RPG 📖) session. During those sessions, when I create/find/insert nodes, I almost want to leverage the same tags. That can be in my capturing of nodes or in my searching for nodes. This is something I observed while running my 13 session “Thel Sector” campaign.

A second use-case is when I’m writing notes or thoughts related to work. In a past life, I might have written notes for either my employer or Samvera (a community in which I participated). Those notes might overlap but rarely did.

While I’m writing those notes, if I’m developing out concepts, I might want to filter my captures and searches to similar tags.

Another use case is less refined, namely I’m writing but am not “in” a specific context.

However, v2 of my org-roam structure, didn’t quite get out of the way. Iterating on my v2 org-roam setup was critical in learning more about Emacs. I will certainly reference my v2 org roam configuration as I continue my Emacs usage. I never quite got to the speed of note taking that I had for the original Thel Sector campaign.

What follows builds on Jethro Kuan’s How I Take Notes with Org-roam. Reading Jethro Kuan’s post helped me see how I could do this.

Preliminaries

The jf/org-roam-capture-templates-plist variable defines the possible org-roam capture templates that I will use. I have chosen to narrow these to three types:

refs
References to other people’s thoughts.
main
My thoughts, still churning, referencing other thoughts.
pubs
My thoughts, published and ready to share. Referencing any thoughts I’ve captured (and probably more).

Note: I chose to go with 4 character types to minimize it’s impact on rendering “type” in the search results (4 characters requires less visual space than 10 characters).

(defvar jf/org-roam-capture-templates-plist
  (list
   ;; These are references to "other people's thoughts."
   :refs '("r" "refs" plain "%?"
	   :if-new (file+head "refs/%<%Y%m%d>---${slug}.org" "#+title: ${title}\n#+FILETAGS:\n")
	   :unnarrowed t)
   ;; These are "my thoughts" with references to "other people's thoughts."
   :main '("m" "main" plain "%?"
	   :if-new (file+head "main/%<%Y%m%d>---${slug}.org"
			      "#+title: ${title}\n#+FILETAGS: ${auto-tags}\n")
	   :immediate-finish t
	   :unnarrowed t)
   ;; These are publications of "my thoughts" referencing "other people's thoughts".
   :pubs '("p" "pubs" plain "%?"
	   :if-new (file+head "pubs/%<%Y%m%d>---${slug}.org" "#+title: ${title}\n#+FILETAGS:\n")
	   :immediate-finish t
	   :unnarrowed t))
  "Templates to use for `org-roam' capture.")

The jf/org-context-plist defines and names some of the contexts in which I might be writing. Each named context defines the associated tags. These are the tags that all nodes will have when they are written in the defined context.

Loosely related is the jf/org-auto-tags--current-list; Contexts are a named set of tags. However, other functions don’t operate based on context. They instead operated based on the tags.

(defvar jf/org-context-plist
  (list
   :none
   (list
    :name "none"
    :tags (list))

   :burning-locusts
   (list
    :name "burning-locusts"
    :tags '("burning-locusts"
	    "rpg"
	    "burning-wheel"))

   :forem
   (list
    :name "forem"
    :tags '("forem"))

   :mistimed-scroll
   (list
    :name "mistimed-scroll"
    :tags '("eberron"
	    "mistimed-scroll"
	    "rpg"
	    "burning-wheel"))
   :thel-sector
   (list
    :name "thel-sector"
    :tags '("thel-sector"
	    "rpg" "swn")))
  "A list of contexts that I regularly write about.")

(defvar jf/org-auto-tags--current-list
  (list)
  "The list of tags to automatically apply to an `org-roam' capture.")

I can use jf/org-auto-tags--set to create an ad hoc context, or perhaps a “yet to be named” context. I can use jf/org-auto-tags--set-by-context to establish the current context (or clear it).

(defun jf/org-auto-tags--set (tags)
  "Prompt user or more TAGS."
  (interactive
   (list
    (completing-read-multiple
     "Tag(s): " (org-roam-tag-completions))))
  (setq jf/org-auto-tags--current-list tags))

(cl-defun jf/org-context-list-completing-read
    (&key
     (context-plist
      jf/org-context-plist))
  "Create a list of contexts from the CONTEXT-PLIST for completing read.

       The form should be '((\"forem\" 1) (\"burning-loscusts\" 2))."
  ;; Skipping the even entries as those are the "keys" for the plist,
  ;; the odds are the values.
  (-non-nil (seq-map-indexed
	     (lambda (context index)
	       (when (oddp index)
		 (list (plist-get context :name) index)))
	     context-plist)))

(cl-defun jf/org-auto-tags--set-by-context
    (context
     &key
     (context-plist jf/org-context-plist))
  "Set auto-tags by CONTEXT.

   Prompt for CONTEXT from CONTEXT-PLIST."
  (interactive
   (list
    (completing-read
     "Context: " (jf/org-context-list-completing-read))))
  (setq jf/org-auto-tags--current-list
	(plist-get
	 (plist-get
	  context-plist (intern (concat ":" context)))
	 :tags)))

With the jf/org-auto-tags--current-list variable set, I want a function to inject those tags onto my captures. Looking at the org-roam docs on template expansion, I want to create a function named org-roam-node-auto-tags.

(cl-defun org-roam-node-auto-tags
    (node
     &key
     (tag-list jf/org-auto-tags--current-list))
  "Inject the TAG-LIST into the {auto-tags} region of captured NODE.

     See https://www.orgroam.com/manual.html#Template-Walkthrough"
  (if (and tag-list (> (length tag-list) 0))
      (concat ":" (s-join ":" tag-list) ":")
    ""))

And finally, we have functions to use for establishing what templates are available based on the context, as well as what to setup as the default filter-fn for org-capture.

In other words, when I have set one or more tags, I want to use the templates appropriate for those tags and filter my org-roam-nodes so that only those nodes that have all of the tags are candidates.

(cl-defun jf/org-roam-templates-list
    (template
     &key
     (template-plist jf/org-roam-capture-templates-plist))
  "List of `org-roam' capture templates based on the given TEMPLATE.

     Searches the TEMPLATE-PLIST for the templates.

     Note, the :all template assumes we use the whole list."
  (if (eq template :all)
      (-non-nil
       (seq-map-indexed
	(lambda (tmp index)
	  (when (oddp index)
	    tmp))
	template-plist))
    (list (plist-get template-plist template))))

(cl-defun jf/org-roam-templates-context-fn
    (&key
     (tag-list jf/org-auto-tags--current-list))
  "Returns a set of templates based on TAG-LIST.

     A key assumption is that if there's a default tag list, use the
     :main template."
  (if (and tag-list (> (length tag-list) 0))
      (jf/org-roam-templates-list :main)
    (jf/org-roam-templates-list :all)))

(cl-defun jf/org-roam-filter-context-fn
    (node
     &key
     (tag-list jf/org-auto-tags--current-list))
  "Determine TAG-LIST is subset of NODE's tags."
  ;; gnus-subsetp is a more "permissive" version of subsetp.  It doesn't
  ;; consider order.  And looks at strings as equal if their values are the
  ;; same.
  (gnus-subsetp tag-list (org-roam-node-tags node)))

Configuration

I wrote three functions to mirror three core functions of org-mode:

  • jf/org-roam-capture: find or create a node and file it away.
  • jf/org-roam-node-insert: find or create a node and insert a link to that node. This is my “take notes quick” function.
  • jf/org-roam-find-node: find a node and open that node in the frame.

For each of those functions, I establish the filter based on the current context and/or tags. I also limit the available capture templates based on the context.

(defun jf/org-roam-capture
    (&optional
     goto
     keys)
  "Call `org-roam-capture' based on set tags."
  (interactive "P")
  (org-roam-capture
   goto
   keys
   :filter-fn 'jf/org-roam-filter-context-fn
   :templates (jf/org-roam-templates-context-fn)))

(defun jf/org-roam-node-insert ()
  "Call `org-roam-node-insert' based on set tags."
  (interactive)
  (org-roam-node-insert
   'jf/org-roam-filter-context-fn
   :templates (jf/org-roam-templates-context-fn)))

(defun jf/org-roam-find-node
    (&optional
     other-window
     initial-input)
  "Call `org-roam-node-find' based on set tags."
  (interactive current-prefix-arg)
  (org-roam-node-find
   other-window
   initial-input
   'jf/org-roam-filter-context-fn
   :templates 'jf/org-roam-templates-context-fn))

And with all of that, let’s get into the org-roam configuration.

(use-package org-roam
  :straight t
  :config
  ;; I encountered the following message when attempting to export data:
  ;;
  ;; => "org-export-data: Unable to resolve link: EXISTING-PROPERTY-ID"
  ;;
  ;; See https://takeonrules.com/2022/01/11/resolving-an-unable-to-resolve-link-error-for-org-mode-in-emacs/ for details
  (defun jf/force-org-rebuild-cache ()
    "Call some functions to rebuild the `org-mode' and `org-roam' cache."
    (interactive)
    (org-id-update-id-locations)
    ;; Note: you may need `org-roam-db-clear-all' followed by `org-roam-db-sync'
    (org-roam-db-sync)
    (org-roam-update-org-id-locations))
  :custom
  (org-roam-directory (file-truename "~/git/org"))
  (org-roam-node-display-template
   (concat "${type:4}   ${title:*} "
	   (propertize "${tags:40}" 'face 'org-tag)))
  (org-roam-capture-templates (jf/org-roam-templates-list :all))
  :bind (("C-s-f" . jf/org-roam-find-node)
	 ("C-s-c" . jf/org-roam-capture))
  :bind (:map org-mode-map
	      (
	       ("C-s-;" . org-roam-buffer-toggle)
	       ("s-i" . jf/org-roam-node-insert)))
  :init
  ;; Help keep the `org-roam-buffer', toggled via `org-roam-buffer-toggle', sticky.
  (add-to-list 'display-buffer-alist
	       '("\\*org-roam\\#"
		 (display-buffer-in-side-window)
		 (side . right)
		 (slot . 0)
		 (window-width . 0.33)
		 (window-parameters . ((no-other-window . t)
				       (no-delete-other-windows . t)))))
  ;; When t the autocomplete in org documents will query the org roam database
  (setq org-roam-completion-everywhere t)
  (setq org-roam-v2-ack t)
  (org-roam-db-autosync-mode))

;; This needs to be after the `org-roam’ declaration as it is dependent on the
;; structures of `org-roam'.
(cl-defmethod org-roam-node-type ((node org-roam-node))
  "Return the TYPE of NODE."
  (condition-case nil
      (file-name-nondirectory
       (directory-file-name
	(file-name-directory
	 (file-relative-name
	  (org-roam-node-file node)
	  org-roam-directory))))
    (error "")))

All told, the past experience when running New Vistas in the Thel Sector // Take on Rules informed how I thought about my note taking.

Other Contexts

Try as I may, based on my configuration, I can’t get org-protocol to work. So I’ve opted to take a different path; write some Emacs functions instead.

  • jf/org-roam-capture-ref: Capture a “refs” context org-roam-node for the given title and url.
  • jf/menu-dwim--org-capture-elfeed-show: Capture an RSS entry.
  • jf/menu-dwim--org-capture-firefox: Capture the active tab of Firefox.
  • jf/menu-dwim--org-capture-safari: Capture the active tab of Safari.

These tie into my the context and auto-tags.

(cl-defun jf/org-roam-capture-ref (&key title url)
  "Capture the TITLE and URL in the `org-roam' :refs template"
  ;; If your installation of org-roam includes the fix fore
  ;; https://github.com/org-roam/org-roam/issues/2078 then you can leave the
  ;; below commented out.
  ;;
  ;; This looks a bit odd, but to capture the :ref we need the callback from org-roam.
  ;; (require 'org-roam-protocol)
  ;;
  (org-roam-capture-
   :keys "r"
   ;; TODO: I would love to get tags working but I'm missing something
   :node (org-roam-node-create :title title)
   :info (list :ref url)
   :props '(:immediate-finish nil)
   :templates (jf/org-roam-templates-list :refs)))

(cl-defun jf/menu-dwim--org-capture-elfeed-show (&key (entry elfeed-show-entry))
  "Create an `org-roam-node' from elfeed ENTRY."
  (interactive)
  (let ((url (elfeed-entry-link entry))
	(title (elfeed-entry-title entry)))
    (jf/org-roam-capture-ref :url url :title title)))

(defun jf/menu-dwim--org-capture-firefox ()
  "Create an `org-roam-node' from Firefox page.

  Depends on the `grab-mac-link' package."
  (interactive)
  (let* ((link-title-pair (grab-mac-link-firefox-1))
	 (url (car link-title-pair))
	 (title (cadr link-title-pair)))
    (jf/org-roam-capture-ref :url url :title title)))

(defun jf/menu-dwim--org-capture-safari ()
  "Create an `org-roam-node' from Safari page.

  Depends on the `grab-mac-link' package."
  (interactive)
  (let* ((link-title-pair (grab-mac-link-safari-1))
	 (url (car link-title-pair))
	 (title (cadr link-title-pair)))
    (jf/org-roam-capture-ref :url url :title title)))

(defun jf/menu-dwim--org-capture-eww ()
  "Create an `org-roam-node' from `eww' data"
  (interactive)
  (let* ((url (plist-get eww-data :url))
	 (title (plist-get eww-data :title)))
    (jf/org-roam-capture-ref :url url :title title)))

Conclusion

This is the core of my note taking engine. It builds on the idea that I want to reduce the number of decisions I make. This is extremely important when I’m writing session notes.

While I’m playing in a session, my entire context ideally collapses to the relevant tags that I’ve established at the beginning of the session. That way I’m certain that I’m filing away notes to their proper location.

-1:-- Org Roam, Emacs, and Ever Refining the Note Taking Process (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--February 08, 2022 01:17 AM

Jeremy Friesen: Switching from Company to Corfu for Emacs Completion

Thinking Through the Principle of Discovery

Both Company and Corfu are completion packages for Emacs 📖. Corfu’s README outlines alternatives, one of which is Company.

Why Corfu?

There are three considerations:

Corfu’s package is one .el file at 1220 lines of code and comments. Whereas Company’s package is many .el files, the company.el file alone is 3916 lines of code and comments.

That’s not to say that that is intrinsically bad, but Corfu’s narrow focus and adherence to the Emacs API 📖 means that the long-term maintenance of Corfu is likely easier.

But that is somewhat ideological. I primarily write Ruby on Rails software; a gigantic code-base. So as with all things ideological, I look towards pragmatism.

The actual “killer” feature of Corfu, which I’m sure I could implement in Company, is the export the popup completion candidates to the mini-buffer.

Embark on a Tangent

I spend quite a lot of time in Emacs’s mini-buffer: searching a project, invoking M-x (e.g., execute-extended-command) to look for Emacs commands, searching for matching lines via consult-line, etc.

Based on my configuration of Emacs, I annotate the mini-buffer with Marginalia. This helps me better understand the context around the candidates.

Throughout the day, I often rely on Embark’s embark-export for exporting those mini-buffer candidates to another buffer, the Embark Export Occur buffer; a buffer I the further search, edit, and manipulate.

Stay on Target

With Corfu, I can send the “pop-up” completion to the mini-buffer. And once in the mini-buffer I can use embark-export.

Below is the code, from the Corfu wiki for transferring the in region popup to the mini-buffer.

(defun corfu-move-to-minibuffer ()
  (interactive)
  (let (completion-cycle-threshold completion-cycling)
    (apply #'consult-completion-in-region completion-in-region--data)))
(define-key corfu-map "\M-m" #'corfu-move-to-minibuffer))

This means I can more thoroughly inspect the candidates recommended by the completion-at-point functions. See their marginalia, and if applicable export each one to an Embark Export Occur buffer for even further interaction.

Back to Principles

ago I wrote Principles of My Text Editor. As I’ve worked with Emacs, I’ve grown to appreciate it’s discoverability.

And Corfu’s ability to move a completion popup to the mini-buffer is a wonderful discoverability feature.

In moving from Company to Corfu I do lose out on the spiffy company-org-block package; which provides a nice convenience method.

In my mental rubric, I would rather have the ability to more thoroughly explore the completion candidates than a convenience function regarding source code in org files.

-1:-- Switching from Company to Corfu for Emacs Completion (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 17, 2022 06:27 PM

Jeremy Friesen: Emacs Packages to Get Stuff Done

Recording and Replaying Keystrokes

, I was working on the Forem codebase. Part of the toolchain at Forem 📖 is that we run Rubocop auto-correct on any files we commit. We use Husky to manage our pre-commit hook.

With auto-correction, we sometimes might get a surprise change. To avoid that surprise change, when we our linters, we try to remember to run the linters against the entire repository. That is not always something we do.

And I got one of those surprises. I wrote a pull request that helped avoid future sneaky auto-corrects.

But I want to write about what I did via Emacs 📖 and the command line.

The Steps in the Process

I took the following steps:

  1. Run Rubocop Auto-Correct.
  2. Run Rspec on a Directory.
  3. Use Emacs to Quickly Remove the Specs.

Run Rubocop Auto Correct

Rubocop—a Ruby 📖 code style checking and code formatting tool— has numerous “cops” that each check the code for one conceptual thing.

You can run Rubocop on a subdirectory and specifying a single “cop” to run. Below is the “cop” I ran on the app/models directory, I specified to auto-correct any offenses:

$ rubocop app/models \
  --only "Rails/RedundantPresenceValidationOnBelongsTo" \
  --auto-correct

There output was as follows:

102 files inspected, \
27 offenses detected, \
27 offenses auto-correctable

With the --auto-correct switch, the above call to rubocop changed the code.

Run RSpec on a Directory

At Forem, we use RSpec. With rubocop changing the models, I wanted to see how this impacted their specs.

$ rspec spec/models

There were 22 failures, which is less than the number of auto-corrects. Looking at the failures they all had the following form:

Failure/Error: it { is_expected.to \
  validate_presence_of(:author_id) }

Excellent, these appear to all be consistent in structure. There were two of the form it { is_expected.to validate_presence_of(:relation) }. I found those when I re-ran the specs. And I quickly remediated them by hand.

Use Emacs to Quickly Remove the Specs

I ran the following commands:

consult-ripgrep
Limiting to files in spec/models, I search for validate_presence_of _id). My consult-ripgrep configuration uses fuzzy finding. In essence, my find will look for lines that have both validate_presence_of and _id) in the same line.
embark-export
Export the search results to a new buffer.
kmacro-start-macro
I recorded, by typing, the following key sequence RET 2*C-k C-d M-o n, more on that later.
kmacro-end-macro
End the recording of the macro.
kmacro-call-macro 21 times
Run the just recorded macro 21 times (one for each of the remaining specs to adjust).
M-x save-some-buffers
Write the changes made in the embark buffer to their corresponding files.

For purposes of explaining what’s happening, I said I called kmacro-start-macro, kmacro-end-macro, and kmacro-call-macro. That’s not quite true, I used kmacro-start-macro-or-insert-counter and kmacro-end-or-call-macro. Those are mapped (by default?) to F3 and F4. But from a writing and explaining standpoint, the underlying functions make a bit more sense.

The Keyboard Sequence

The key sequence for kmacro-start-macro assumes:

  • I’m starting in the search results buffer.
  • I have two windows open, one for the search result buffer and one for “work”.

Below, I write about the key macro and what it does:

RET
Open the source file and line for selected search result.
Ctrl + k twice
Delete to the end of line and delete that now blank line. See jnf/kill-line-or-region for more details.
Ctrl + d
Delete the leading blank space.
Alt + o
Jump back to the search result buffer. See ace-window for more details.
n
Move to the next line in the search results buffer.

While recording keyboard macro, I could see what I was changing. I took my time to type and think what I wanted to do.

I recorded the macro, felt comfortable with it, and told Emacs to run it 21 more times. I could have instead mashed on the F4 key twenty one (21) times.

Conclusion

First, I did all of this from a clean Git 📖 repository state. This allowed me to make potentially sweeping changes with confidence of being able to revert.

I often use Embark’s embark-export; creating a buffer with a list of candidates. I can interact with the candidates, save the buffer for later, or further narrow those candidates with a different command.

What’s the list of candidates? In the above example, it’s search results. But it can be more than that. I often pair embark-export with wgrep. Wgrep allows you to edit a grep buffer and apply those changes to the file buffer like sed 📖 interactively. Wgrep’s functionality is now a must have for my text editor.

It took me over a year of using Emacs to even start recording and using keyboard macros. In I wrote Principles of My Text Editor.

Nowadays, I’m usually recording a disposable macro every other day. The more I use Emacs the more I learn and adjust how I can tell Emacs to do more work for me.

-1:-- Emacs Packages to Get Stuff Done (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 13, 2022 03:38 PM

Jeremy Friesen: Resolving an Unable to Resolve Link Error for Org Mode in Emacs

Documenting to Share a Solution

I’ve been dabbling with the org-transclusion package. But I encountered an odd behavior somewhere in my Org-Mode 📖 and Org-Roam 📖 configuration.

What Worked and What Didn’t

I had a source org file. I linked to the target org file in another directory via the target’s id property. In my source I had the equivalent of [[id:org-id-get-create][Name of Target]]. The org-id-get-create is a placeholder (and the name of the function I used to generate the target’s ID).

When I would open the target link in my buffer, I would jump to the target document. As expected.

However, when I attempted to use the org-transclusion functions or export (via org-export-dispatch) the org document to another format, I got the following error:

Unable to resolve link: org-id-get-create

I thought this had something to do with my directory structure. But it worked to link to other files in the same directory as the target.

Poking Around Through Source Code

I tried rebuilding my org-roam database (e.g., M-x org-roam-db-clear-all then M-x org-roam-db-sync) but that didn’t work.

In the source file, I ran org-lint. That generated reported what looked to be the same error message.

So I dug further into the org-lint method. There I learned about the org-lint--checkers variable. The registered functions that are called to lint a document. I went looking through that list and found a likely culprit: org-lint-invalid-id-link.

The org-lint-invalid-id-link function lead me to the org-id-find function. Which in turn called org-id-find-id-file id. And there I found the following code bit of code: (unless org-id-locations (org-id-locations-load)).

Huzzah! Org-mode was generating a cache! Somewhere there must be a cache invalidation or update function.

So I went looking for functions like the org-id-locations-load. I found org-id-update-id-locations and org-roam-update-org-id-locations.

I ran the org-id-update-id-locations and it reported processing several files, but certainly not enough. I thenorg-roam-update-org-id-locations; that function call reported a lot of processed files.

Hoping that this update of a cache would resolve my problem, I went back to the previously broken source document. Both org-transclusion and org-export-dispatch worked.

Capturing the Knowledge

I wrote a function that is now part of my Emacs 📖 configuration; the purpose of the function is to help remind me how to fix this problem if it shows up again.


;; I encountered the following message when attempting
;; to export data:
;;
;; "org-export-data: Unable to resolve link: FILE-ID"
(defun jnf/force-org-rebuild-cache ()
  "Rebuild the `org-mode' and `org-roam' cache."
  (interactive)
  (org-id-update-id-locations)
  ;; Note: you may need `org-roam-db-clear-all'
  ;; followed by `org-roam-db-sync'
  (org-roam-db-sync)
  (org-roam-update-org-id-locations))

I don’t envision running that function all that much, but it will be a bit of written memory of this debugging.

Conclusion

First, navigating Emacs functions is amazing. Emacs provides powerful introspection.

I used helpful.el to jump to the documentation of a function, and kept jumping deeper. I used M-x paired with marginalia.el to look for similar named functions.

Pairing helpful.el and marginalia.el (and many other packages), I could do ever narrowing work and when I needed to expand my focus.

In fact, it was so easy to navigate I did it twice. First when debugging and then when writing this blog post.

Second, taking the time to write this up, I hope I help other folks (including my future self).

-1:-- Resolving an Unable to Resolve Link Error for Org Mode in Emacs (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 12, 2022 03:37 AM

Jeremy Friesen: Using PlantUML to Model RPG Relationship Maps

Leveraging Open Source Software for RPGs

For our Burning Locusts series, the game facilitator wrote the campaign’s relationship graph in dot notation: a text format for representing relationships. See Graphviz) for more details on dot notation.

In years past, I used dot notation. I used dot notation to make the diagrams for Cluster Creation Diaspora.

I decided to write up a relationship diagram using PlantUML: an open-source tool allowing users to create diagrams from a plain text language. See PlantUML for more details.

These days, I prefer PlantUML as it’s intergrated with Org-Mode 📖.

Below I have both the diagram’s source and a png rendering. I chose to render using the amiga theme.

Source and Rendering

PlantUML source for The Mistimed Scroll relationship diagram

@startuml
!theme amiga

'PCs (using the "actor" node)
actor Cloak as cloak
actor Soliana as soliana
actor Keth as keth

'NPCs (using the "actor/" node)
actor/ Tondilo as tondilo
actor/ Gwynn as gwynn
actor/ Vars as vars
actor/ Sollar as sollar

'Factions (using the "cloud" node)
cloud house_jorasco as "House Jorasco"
cloud house_deneith as "House Deneith"
cloud house_tharashk as "House Tharash"

'Locations
frame "New Cyre" as new_cyre {
  actor/ "Prince Oargev" as prince_o
}

'Objects
entity "The\nScroll" as scroll

cloak <-l-> tondilo   : "friends*"
cloak -d-> scroll    : "possesses"
soliana -d-> scroll  : "knows about"
soliana <-l-> tondilo : "friends"
soliana <--> gwynn   : "healed*"
soliana <--> sollar  : "siblings*"
keth <--> vars       : "coworkers*"
cloak <--> vars      : "shot"

house_jorasco --> vars : "tending to"
gwynn --> house_deneith : "works for"
keth --> house_tharashk : "works for"
vars --> house_tharashk : "works for"
@enduml
Rendered relationship diagram for The Mistimed Scroll (via PlantUML)

The relationship diagram after the first two sessions of The Mistimed Scroll.

-1:-- Using PlantUML to Model RPG Relationship Maps (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--January 09, 2022 09:54 PM

Jeremy Friesen: The Serendipity of Pairing with a New Developer

Learning as I Watch Others Navigate Their Toolbox

Earlier this week, Dwight joined the Forem team. we paired on scoping a problem.

While Dwight drove, I helped navigate. As he was typing in his terminal, I noticed an interesting feature. With a blank input prompt a drop-down appearred and he’d select a command from history.

By default, when I typed Ctrl+r I got history-incremental-search-backward. Which was a rather simple prompt for clumsily searching past commands. What I saw in Dwight’s terminal was something far more robust. When he typed Ctrl+r, he got a list of past commands and could type to filter towards those commands.

I asked about the configuration, and Dwight told me it was a plugin.

New to Me Tools

After our pairing session, I went looking.

First, I stumbled into hstr, a command to easily view, navigate and search command history with shell history suggest box for bash and zsh. I installed it and configured that plugin.

This set me on the path for further exploration. I then found fzf, a general-purpose command-line fuzzy finder. I started exploring that, and the extensive community wiki entries that leverage fzf.

I added to my terminal functions:

  • fkill, a fuzzy search of processes to kill.
  • fe, a fuzzy file finder that opens the selected file(s) in my editor.
  • rfv, a two stage file name and content finder.

I also replaced the recently installed hstr with fzf’s fzf-history-widget

And while reading through the wiki, I found forgit, a Utility tool for using git interactively. Powered by junegunn/fzf. I favor Emacs 📖’s amazing magit package for most git interactions. But forgit’s interactive log viewer provides functionality that I haven’t found in Magit 📖.

Wrapping Up

If you often interact with git via the command-line, I encourage you to look into forgit. It provides userful interactive additions to your git repertoire.

These three tools—hstr, fzf, and forgit—are all fantastic command-line additions. While I tend to spend more of my time in Emacs than on the command-line, I do find myself in the command-line doing some tasks. These commands, in particular fogit::log (and it’s alias glo) are useful tools for my toolkit.

I also spent some time reading through the archaic output of my bindkey output. I learned that Ctrl+x then Ctrl+e would open a new buffer for my configured editor with the current command line’s prompt’s content as the buffer’s content.

All of this learning and exploring came about because I paired with a developer and was curious about how they navigated their toolbox.

-1:-- The Serendipity of Pairing with a New Developer (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 20, 2021 05:25 PM

Jeremy Friesen: Adding Emacs Function for my Forem Pull Requests

Automating the Repetetive while also Learning a Bit More about My Editor

In I joined Forem as the lead engineer for the content experience team. I’ve been contributing to open source software for 9 years; Hello Samvera.org; I miss you but I promise I’m in a good place.

Coming from one open source community to another, I brought with me different workflows. I favor writing verbose commit messages. I like to use that as the text for my pull requests. The benefits are that commit messages travel with the code-base. I can use git annotate to help me understand the situation around a chunk of code.

But, in an open source community with over 600 contributors, the commit message as pull request strategy is inadequate. We could use git hooks to provide commit message templating, but that’s not enough for conversations around the pull request.

Forem provides a pull request template to guide contributors through all of the considerations that go into the pull request review and acceptance.

The template provides a nice pre-amble comment to help new contributors. Then provides clear sections and instructions for a contributor to fill out:

  • What type of Pull Request
  • Description
  • Related Tasks & Documents
  • QA Instructions, Screenshots, and Recordings
  • Accessibility Concerns
  • Added/updated Tests
  • How will this change be communicated? (A Forem Core Team only section)
  • Any post deployment tasks to complete
  • A GIF that Expresses How You Feel About this Contribution

As a new contributor to Forem, I love this guidance. And as I began reviewing other pull requests, I appreciated the structure even more.

My Current Pull Request Workflow

When I’m working on the code, I continue to write verbose commit messages. Then, when I’m ready, I push up my branch and push the button to create a pull request for the branch.

By default, Github prepends the last commit message to the text of the pull request template. I focus my browser into that text area and use the editWithEmacs.spoon to copy that text and paste it into a new Emacs buffer on my machine.

In that Emacs buffer, I then go about editing the pull request text.

When I’m done, I type Ctrl+c then Ctrl+c (e.g., C-c C-c in Emacs parlance) to copy the text from my Emacs buffer and paste it back into the browser’s text area. Magit and Org Mode use that key combination for confirmation of commands.

And I submit my pull request.

Automating My Workflow

Once I started editing these pull requests in Emacs, I started to see the clean-up work that I was regularly doing before I started filling out the checkboxes. And because I was now in my text editor, I chose to write a script to do that clean-up.

Without reading the elisp code, it:

  • Removes the comment preamble
  • It adds the last commit message as the description
  • It tidies up the comments of two sections

Below is the lisp code to do the tidying up:

(defun jnf/forem-tidy-pull-request ()
  "Perform some quick tidying of the Forem PR template."
  (interactive)
  ;; Start from the beginning.
  (beginning-of-buffer)

  ;; The text before the first HTML/Markdown
  ;; comments is the commit message.  Cut that
  ;; text...
  (search-forward "<!--")
  (kill-region 1 (- (point) 4))

  ;; ...and paste it inside the description
  ;; section.
  (replace-string
   "## Description\n\n"
   (concat "## Description\n\n"
           (format "%s" (car kill-ring))))

  ;; We've moved point (e.g., the cursor) so let's
  ;; jump back to the beginning of the buffer.
  (beginning-of-buffer)

  ;; Remove HTML/Markdown comments
  (replace-regexp
   "\\(\n\\)*<!--\\(.\\|\n\\)*-->\\(\n\\)*"
   "")

  ;; Clean out the comments for QA instructions;
  ;; I'll write them, but the notes are
  ;; unnecessary.
  (replace-regexp
   "QA Instructions, Screenshots, Recordings\\([^#]\\)*"
   "QA Instructions, Screenshots, Recordings\n\n")

  ;; Clean out accessibility concerns; I'll write
  ;; them, but the notes are unnecessary.
  (replace-regexp
   "UI accessibility concerns?\\([^#]\\)*"
   "UI accessibility concerns?\n\n"))

Then comes the keyboard bindings to make this easier.

When copying from browser to Emacs, the editWithEmacs.spoon toggles on the hammerspoon-edit-minor-mode for the buffer. See the code for those details. The following code adds a new key binding Ctrl+c then t to the keyboard mappings.

(define-key
  hammerspoon-edit-minor-map
  (kbd "C-c t")
  #'jnf/forem-tidy-pull-request)

Kind of nice. Load the content into an Emacs buffer, type Ctrl+c then t and I’m a few steps closer to completing my pull request.

What remains?

I wrote a script to build a pull request message from commit messages. Note, at my previous employer they chose to keep using—and keep choosing to use—the branch name master hence the code defaults to that.

I would like to better incorprate that conceptual script into my workflow.

And if I’m feeling up for the challenge, I’ll grab any Github links from the commit messages and add those to the related tasks and documents.

Conclusion

Since joining Forem, I’ve issued 32 pull requests. And as I started doing this task more, I started wondering, “How might I tweak my tooling to address some repetetive tasks?”

I let that question linger as I wrote several pull request messages in Emacs. And then, with a bit of time, I chose to spend a bit of time writing the above script. I don’t know how many pull requests I’ll need to write to “make up” for the time spent on the script.

But that is a lesser concern. I’m more concerned with getting comfortable understanding the interplay of the various systems I use and how I can mold them to assist in the tasks at hand.

When I start to create a pull request, I can quickly run the clean up task so that I can then focus on writing the pull request. In other words, I automated away a “distraction” so I could stay closer to the area of focus.

-1:-- Adding Emacs Function for my Forem Pull Requests (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--November 25, 2021 09:44 PM

Jeremy Friesen: Send Anything in OS X to Emacs for Editing

Hacking Away with Hammerspoon and editWithEmacs

The worst part about Emacs 📖 is that sometimes you have to edit things outside of Emacs.

Yesterday, I found dmgerman/editWithEmacs.spoon. It uses Hammerspoon to send the current text to and from Emacs.

Explaining editWithEmacs.spoon

When I’m in a non-Emacs application (let’s say Firefox) and editing a text area (e.g., a Github Pull Request comment). I can type Ctrl+Alt+Cmd+e to transfer the text area’s content to a new Emacs buffer

In that buffer, I type away. When I’m done, I type Ctrl+c then Ctrl+c (e.g., C-c C-c in Emacs lingo) to send the content of that buffer back to the originating application.

Forking and Extending

, I forked the repository and began making updates. My fork now works wonderfully for my Emacs configuration; and exposes some additional configuration points that I think make this just a bit more extensible.

This is a big deal for me, because I’m now well accustomed to using Emacs for most longer form writing. And with my editWithEmacs.spoon, I can quickly jump into Emacs for writing.

Along the way, I also installed Miro Windows Manager for Hammerspoon. I cannot emphasize how awesome that script is for windows management.

Take a look at my Hammerspoon init.lua file. The two “spoons” that I’m using are already making my computing life just a bit nicer.

-1:-- Send Anything in OS X to Emacs for Editing (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--November 13, 2021 11:06 PM

Jeremy Friesen: Further Hacking on Emacs for Github Pull Requests

You Ain't Emacs-ing if You Ain't Always Hacking Your Config

I wrote Emacs Function to Open Magit Log PR at Point. Over on Reddit, a user asked about not requiring git-link dependency nor browse-url-default-macosx-browser.

Since then, I’ve split apart the functions and added another use case. First and foremost, the magic “open the pull request associated with a commit” relies on an implementation feature of Github’s “Squash and merge” command. That command creates a commit with a summary (e.g., the first line of the commit message) that is the pull request’s title and the associated pull request.

Functions

With that as a caveat, there are five functions that I’ve written to help jump to pull requests on Github:

  • jnf/git-current-remote-url
  • jnf/open-pull-request-for
  • jnf/magit-browse-pull-request
  • jnf/open-pull-request-for-current-line
  • jnf/git-messenger-popup

jnf/git-current-remote-url

The following Elisp: dialect of Lisp used in GNU Emacs (Elisp 📖) code defines the jnf/git-current-remote-url function which gets the current remote url (for the given branch). It’s usually “origin.”


(defun jnf/git-current-remote-url ()
  "Get the current remote url."
  (s-trim
   (shell-command-to-string
    (concat
     "git remote get-url "
     (format "%s" (magit-get-current-remote))))))

jnf/open-pull-request-for

The following elsip code defines jnf/open-pull-request-for, which takes the named parameter :summary. If that :summary contains a pull request number, opens the pull request in an external browser.


(cl-defun jnf/open-pull-request-for (&key summary)
  "Given the SUMMARY open the related pull request."
  (let ((remote-url (jnf/git-current-remote-url)))
    (save-match-data
      (and (string-match "(\\#\\([0-9]+\\))$" summary)
           (eww-browse-with-external-browser
            (concat
             ;; I tend to favor HTTPS and the
             ;; repos end in ".git"
             (s-replace ".git" "" remote-url)
             "/pull/"
             (match-string 1 summary)))))))

jnf/magit-browse-pull-request

The following Elisp code defines jnf/magit-browse-pull-request, which will open the associate pull request when your point is on a Magit 📖 log entry. I’ve mapped that to s-6 (or Cmd+6)


(defun jnf/magit-browse-pull-request ()
  "In `magit-log-mode' open the associated pull request
at point.

Assumes that the commit log title ends in the PR #, which
is the case when you use the Squash and Merge strategy.

This implementation is dependent on `magit' and `s'."
  (interactive)
  (let* ((beg (line-beginning-position))
         (end (line-end-position))
         (summary
          (buffer-substring-no-properties
           beg end)))
    (jnf/open-pull-request-for :summary summary)))

jnf/open-pull-request-for-current-line

The following Elisp code defines jnf/open-pull-request-for-current-line. When invoked, this function will open the pull request for the commit associated with the current line. It does that by using git annotate on the current line, and pulling the commit’s summary via ripgrep.


(defun jnf/open-pull-request-for-current-line ()
  "For the current line open the applicable pull request."
  (interactive)
  (let ((summary
         (s-trim
          (shell-command-to-string
           (concat "git --no-pager annotate "
                   "-L "
                   (format "%s" (line-number-at-pos))
                   ",+1 "
                   "--porcelain "
                   buffer-file-name
                   " | rg \"^summary\"")))))
    (jnf/open-pull-request-for :summary summary)))

jnf/git-messenger-popup

The following Elisp code defines jnf/git-messenger-popup. When invoked it launches the git-messenger popup.


(defun jnf/git-messenger-popup ()
  "Open `git-messenger' or github PR.

With universal argument, open the github PR for
current line.

Without universal argument, open `git-messenger'."
  (interactive)
  (if (equal current-prefix-arg nil) ; no C-u
      (git-messenger:popup-message)
    (jnf/open-pull-request-for-current-line)))

I have mapped the function to s-6 (e.g., Cmd+6 on OS X 📖).

If I first pass the universal argument, that is I first type C-u then s-6 (or Ctrl+u then Cmd+6 in OS X) I will open that line’s pull request. When in the git-messenger’s popup, I can type p to go to that line’s pull request.

Conclusion

I wrote these functions to better help me better understand Forem’s codebase. It was also a chance to continue practicing coding and learning.

If you’re interested, you can see more of my git configuration on Github

-1:-- Further Hacking on Emacs for Github Pull Requests (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--November 11, 2021 02:02 AM

Jeremy Friesen: Creating a Documentation Dashboard and Emacs Function

Reducing Friction on Adding Placeholders for My Future Self

This post further builds on Slowing Down to Synthesize and also incorporates ideas from Alex Schroeder: 2021-09-17 Writing to learn.

When I started at Forem, there were lots of new web pages that I didn’t want to forget. I wanted to annotate and tag those web pages. I chose not to use my browser’s bookmarks and instead chose to create an Org-Mode 📖 document. That document resides in my private repository for Forem Org-Roam 📖 directory.

Crash Course Org Mode

Here is one of the several entries in my Dashboard:


** [[https://forem.team/][Forem Team 🌱]] :communication:documentation:

This is where we have long-running conversations

The leading ** indicates a heading level two in org-mode; analog to Markdown’s ##.

The [[url][text]] is a link and it’s text.

The :communication:documentation: are two tags that I’ve assigned to that heading. And last the This is where&hellip; is a paragraph description.

My goal was to write down and remember these different sources of possible information or tools to use.

Scripting the Dashboard

With a place to capture the data, I then wrote a Ruby script to open each of those web pages in my default browser. I wrapped that Ruby script with an Emacs 📖 function. Later, I replaced that Ruby script with a ripgrep invocation.

I mapped that Emacs function Cmd+Opt+Ctrl+d to open my dashbard files in the browser. I also added a bit of logic that said if you first type Emacs’s universal modifier (e.g., C-u, that is Ctrl+u) then invoke the function it will instead open the Dashboard’s source file.

Below is that code:


;; In OS X this is CMD+OPT+CTRL+d
(global-set-key (kbd "C-M-s-d") 'jnf/open-dashboard)
(cl-defun jnf/open-dashboard (&key (filename jnf/forem-dashboard-filename))
      "For the given FILENAME open the links in the default browser.

With the universal prefix (e.g. C-u) open the file instead."
      (interactive)
      (if (equal current-prefix-arg nil)
          (call-process-shell-command
           ;; Double escaped because I'm passing this
           ;; string to the command line.
           (concat "rg \"\\[\\[(.*)\\]\\[\" "
                   filename
                   " --only-matching"
                   " | rg \"[^\\[|\\]]+\" --only-matching"
                   " | xargs open"))
        (find-file filename)))

Let’s dive into the above ripgrep command (for convenience I’m removing the double escaping):

First we have rg "\[\[(.*)\]\[" filename --only-matching. That command finds only the [[url] portion in the given filename.

Then we pipe that to rg "[^\[|\]]+" --only-matching. This further narrows that search to only select the url.

And last, I pipe this to xargs open. In essence, that then runs the open command from OS X on each of the matching URLs. open on a URL string will open that URL in the default browser.

My plans for this function are to prompt for a tag, and limit opening only web pages with matching tags. So far, I haven’t needed it.

Conclusion

In the early days of a new job, there’s a lot of information intake. I created a Dashboard document to provide a consistent place to capture that information; I knew I didn’t want to lose track of it. The Dashboard document reduces the friction of deciding where to put things.

It was rather quick to write up the functions (Ruby, Ripgrep, and Lisp). Most important to me, is that writing these functions helps re-iterate that my text editor configuration is a continual work in progress. My text editor is an extension of my current understanding, and I should use it and extend it to help me learn and capture ideas.

-1:-- Creating a Documentation Dashboard and Emacs Function (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--November 09, 2021 01:51 AM

Jeremy Friesen: Emacs Function to Open Magit Log PR at Point

A Quick Hack to Help My Code Spelunking

At Forem, we make extensive use of the Github pull request conversations on the forem codebase. We also use the Squash and Merge strategy for Github. See What’s the Difference Between the 3 Github Merge Methods? for details on the strategy.

One side-effect of the Squash and Merge is that Github appends the merged pull request number to the commit message. So this evening, I whipped up an Emacs function that I can call from a Magit 📖 log to open the pull request in my default browser.


(defun jnf/magit-browse-pull-request ()
  "In `magit-log-mode', open the associated pull request at point."
  (interactive)
  (let* ((remote-url
          (car
           (git-link--exec
            "remote" "get-url"
            (format "%s"
                    (magit-get-current-remote)))))
         (beg (line-beginning-position))
         (end (line-end-position))
         (region (buffer-substring-no-properties beg end)))
    (save-match-data
      (and (string-match "(\\#\\([0-9]+\\))$" region)
           (browse-url-default-macosx-browser
            (concat
             (s-replace ".git" "" remote-url)
             "/pull/"
             (match-string 1 region)))))))

This works, and I’m certain there are improvements to my code. The above function relies on the s, magit, and git-link package.

In magit-log-mode I bound s-6 to jnf/magit-browse-pull-request.

-1:-- Emacs Function to Open Magit Log PR at Point (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--November 06, 2021 02:52 AM

Jeremy Friesen: Whipping Up a Quick Emacs Helper Function for Hugo

Continuing to Build Out Utility Functions

I’ve previously written about Emacs Function to Rename Hugo Blog Post and since then, I’ve added more functions. Someday, I’ll get around to sharing more of them. They’re almost ready to packaged up, but I haven’t spent the mental cycles thinking what’s in the package and what’s my local needs.

I was thinking about my process for finding the Hugo 📖 file associated with a blog post.

The specific situation was that I wanted to update Ever Further Refinements of Org Roam Usage to include a reference to the follow-up post Diving into the Implementation of Subject Menus for Org Roam.

I had the URL for the post I wanted to update. I also had some existing functions that I’d written to help me find all of the drafts in my Hugo repository.

Code for finding a Hugo file based on a URL.

These constants and functions were things I'd previously written.

Note: This implementation assumes you are using the f package and have installed ripgrep, which is aliased as rg in the command shell.


(defconst jnf/tor-home-directory
  (file-truename "~/git/takeonrules.github.io")
  "The home directory of TakeOnRules.com Hugo repository.")

(defconst jnf/tor-hostname-regexp
  "https?://takeonrules\.com"
  "A regular expression for checking if it's TakeOnRules.com.")

(cl-defun jnf/tor-prompt-or-kill-ring-for-url (&key
                                               (url-regexp "^https?://"))
  "Prompt and return a url.

If the \`car' of \`kill-ring' matches the URL-REGEXP, default the
prompt value to the \`car' of `kill-ring'."
  (let ((car-of-kill-ring (substring-no-properties (car kill-ring))))
    (read-string "URL (optional): "
                 (when (string-match url-regexp car-of-kill-ring)
                   car-of-kill-ring))))

(cl-defun jnf/list-filenames-with-file-text (&key matching in)
  "Build a list of filenames MATCHING IN the given directory."
  (let ((default-directory (f-join jnf/tor-home-directory in)))
    (split-string-and-unquote
     (shell-command-to-string
      (concat
       "rg \""
       matching "\" --only-matching --files-with-matches "
       "| sort | tr '\n' '~'"))
     "~")))

They provided the bits and pieces for crafting jnf/tor-find-hugo-file-by-url, the function that prompts for a URL and finds the associated Hugo file.


(cl-defun jnf/tor-find-hugo-file-by-url (url)
  "Find the associated TakeOnRules.com file for the given URL."
  (interactive (list
                (jnf/tor-prompt-or-kill-ring-for-url
                 :url-regexp jnf/tor-hostname-regexp)))
  ;; With the given URL extract the slug
  (let* ((slug (car (last (split-string-and-unquote url "/"))))
         (filename (car
                    (jnf/list-filenames-with-file-text
                     :matching (concat "^slug: .*" slug "$")
                     :in "content"))))
    (find-file (f-join
                jnf/tor-home-directory
                "content"
                filename))))

Conclusion

With the above Elisp 📖, I can now use M-x jnf/tor-find-hugo-file-by-url, type (or paste) the URL into the prompt, and Emacs 📖 will open the corresponding blog post.

This does require that all of my blog posts have a slug frontmatter entry. This function does not work for non-blog post pages on my site. They have a different frontmatter structure.

To handle both pages and posts, I’m going to need to introduce some switching logic. But I don’t yet need it, so I’ll hold off.

-1:-- Whipping Up a Quick Emacs Helper Function for Hugo (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 28, 2021 04:46 PM

Jeremy Friesen: Adding More Tag Rendering Functions for SHR in Emacs

Adding More Default Styles of Browsers

As I’ve been using Emacs 📖, I’m favoring the Emacs Web Wowser (EWW 📖). The rendering logic uses the Simple HTML Renderer (SHR 📖) package. Both EWW and SHR are part of the core Emacs distribution).

I like the experience of reading blogs via a text-based browser. I also like eschewing Cascading Stylesheet 📖 and Javascript 📖 from websites.

However, as I was writing a new blog post, and previewing it with EWW, I noticed that some of the HTML tags I use didn’t render as I would’ve thought. I spent some time reading through the SHR source code to get clearer sense of defaults.

I then took inspiration from some of the other rendering functions for my favorite Emacs Web Wowser. These options align with many of the default user agent style sheets.

Each browser has a default stylesheet. You can find an excellent list at Jens Oliver Meiert’s User Agent Style Sheets: Basics and Samples.

I use the following tags throughout Take on Rules:

And the base SHR does not have corresponding shr-tag- functions for them.

Here Is the Code for the Tags

;; Inspired from shr-tag-em
(defun shr-tag-dfn (dom)
  (shr-fontize-dom dom 'italic))

;; Inspired from shr-tag-em
(defun shr-tag-cite (dom)
  (shr-fontize-dom dom 'italic))

;; Inspired from shr-tag-a
(defun shr-tag-q (dom)
  (shr-insert "“")
  (shr-generic dom)
  (shr-insert "”"))

;; Drawing inspiration from shr-tag-h1
(defun shr-tag-small (dom)
  (shr-fontize-dom
   dom (if shr-use-fonts '(variable-pitch (:height 0.8)))))

;; Drawing inspiration from shr-tag-abbr
(defun shr-tag-time (dom)
  (when-let* ((datetime (or
                         (dom-attr dom 'title)
                         (dom-attr dom 'datetime)))
	      (start (point)))
    (shr-generic dom)
    (shr-add-font start (point) 'shr-abbreviation)
    (add-text-properties
     start (point)
     (list
      'help-echo datetime
      'mouse-face 'highlight))))

Conclusion and Next Steps

I added the above functions to my init.el file; These little tweaks improve my already fantastic EWW browsing experience.

I was also thinking it would be nice if I could get Imenu to render the headings of HTML pages. But that’s something for another time.

-1:-- Adding More Tag Rendering Functions for SHR in Emacs (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 26, 2021 11:57 AM

Jeremy Friesen: Diving into the Implementation of Subject Menus for Org Roam

It's Macros, Functions, and Property Lists…Oh My!

I wrote Ever Further Refinements of Org Roam Usage. In that post I talked about what I was implementing and why. I’m writing about the implementation details.

After writing Ever Further Refinements of Org Roam Usage, I spent a bit of time refactoring the code. I put that code up as a gist on Github. You can see the history of the refactoring, albeit without comments.

One result of the refactoring is that the menus now look a bit different. But the principle remain the same.

The Lists to Define Subjects

First, let’s start with the jnf/org-roam-capture-templates-plist. I created a Property List, or plist, for all of my org-roam templates.

Property list jnf/org-roam-capture-templates-plist implementation

(setq jnf/org-roam-capture-templates-plist
      (list
       :hesburgh-libraries
       '("h" "Hesburgh Libraries" plain "%?"
	 :if-new
         (file+head
          "hesburgh-libraries/%<%Y%m%d>---${slug}.org"
          "#+title: ${title}\n#+FILETAGS: :hesburgh: %^G\n\n")
	 :unnarrowed t)
       :jf-consulting
       '("j" "JF Consulting" plain "%?"
	 :if-new
         (file+head
          "jeremy-friesen-consulting/%<%Y%m%d>---${slug}.org"
          "#+title: ${title}\n#+FILETAGS: :personal:jeremy-friesen-consulting: %^G\n\n")
	 :unnarrowed t)
       :personal
       '("p" "Personal" plain "%?"
	 :if-new
         (file+head
          "personal/%<%Y%m%d>---${slug}.org"
	  "#+title: ${title}\n#+FILETAGS: :personal: %^G\n\n")
	 :unnarrowed t)
       :personal-encrypted
       '("P" "Personal (Encrypted)" plain "%?"
	 :if-new
         (file+head
          "personal/%<%Y%m%d>---${slug}.org.gpg"
          "#+title: ${title}\n#+FILETAGS: :personal:encrypted: %^G\n\n")
	 :unnarrowed t)
       :public
       '("u" "Public" plain "%?"
	 :if-new
         (file+head
          "public/%<%Y%m%d>---${slug}.org"
	  "#+title: ${title}\n#+FILETAGS: :public: %^G\n\n")
	 :unnarrowed t)
       :thel-sector
       '("t" "Thel Sector" plain "%?"
         :if-new
         (file+head
          "personal/thel-sector/%<%Y%m%d>---${slug}.org"
          "#+title: ${title}\n#+FILETAGS: :thel-sector: %^G\n\n")
         :unnarrowed t)
       ))

With the above, I have a symbolic name for each template. I can then use lookup functions to retrieve the implementation details.

I then created a plist for the subjects (e.g., jnf/org-roam-capture-subjects-plist). Each subject is itself a plist.

Property list jnf/org-roam-capture-subjects-plist implementation

(setq jnf/org-roam-capture-subjects-plist
      (list
       ;; The :all subject is different from the other items.
       :all (list
             ;; Iterate through all registered capture templates and
             ;; generate a list
             :templates (-non-nil (seq-map-indexed (lambda (template index)
                     (when (evenp index) template))
                   jnf/org-roam-capture-templates-plist))
             :name "all"
             :title "All"
             :group "All"
             :prefix "a"
             :path-to-todo "~/git/org/todo.org")
       :jf-consulting (list
                       :templates (list :jf-consulting)
                       :name "jf-consulting"
                       :title "JF Consulting"
                       :group "Projects"
                       :prefix "j"
                       :path-to-todo "~/git/org/jeremy-friesen-consulting/todo.org")
       :hesburgh-libraries (list
                            :templates (list :hesburgh-libraries)
                            :name "hesburgh-libraries"
                            :title "Hesburgh Libraries"
                            :group "Projects"
                            :prefix "h"
                            :path-to-todo "~/git/org/hesburgh-libraries/todo.org")
       :personal (list
                  :templates (list :personal :personal-encrypted)
                  :name "personal"
                  :title "Personal"
                  :group "Life"
                  :prefix "p"
                  :path-to-todo "~/git/org/personal/todo.org")
       :public (list
                :templates (list :public)
                :name "public"
                :title "Public"
                :group "Life"
                :prefix "u"
                :path-to-todo "~/git/org/public/todo.org")
       :thel-sector (list
                     :templates (list :thel-sector)
                     :name "thel-sector"
                     :title "Thel Sector"
                     :group "Projects"
                     :prefix "t"
                     :path-to-todo "~/git/org/personal/thel-sector/todo.org")
       ))

The jnf/org-roam-capture-subjects-plist plist contains the various org-roam subjects. Each subject is a plist with the following properties:

:templates
A list of named templates available for this subject. See jnf/org-roam-capture-templates-plist for list of valid templates.
:name
A string version of the subject, suitable for creating function names.
:title
The human readable "title-case" form of the subject.
:group
Used for appending to the "All" menu via pretty-hydra-define+.
:prefix
Used for the prefix key when mapping functions to key bindings for pretty-hydra-define+.
:path-to-todo
The path to the todo file for this subject.

Functions to Help Build the Hydra Menus

I wrote the jnf/org-roam-templates-for-subject function to retrieve a subject’s Org-Roam 📖 templates.

Function jnf/org-roam-templates-for-subject implementation

(cl-defun jnf/org-roam-templates-for-subject (subject
                                              &key
                                              (subjects-plist jnf/org-roam-capture-subjects-plist)
                                              (template-definitions-plist jnf/org-roam-capture-templates-plist))
  "Return a list of \`org-roam' templates for the given SUBJECT.

Use the given (or default) SUBJECTS-PLIST to fetch from the
given (or default) TEMPLATE-DEFINITIONS-PLIST."
  (let ((templates (plist-get (plist-get subjects-plist subject) :templates)))
    (-map (lambda (template) (plist-get template-definitions-plist template))
          templates)))

I then created jnf/org-subject-menu–all, a pretty-hydra-define menu.

Pretty-hydra-define jnf/org-subject-menu--all implementation

(defvar jnf/org-subject-menu--title (with-faicon "book" "Org Subject Menu" 1 -0.05))
(pretty-hydra-define jnf/org-subject-menu--all (:foreign-keys warn :title jnf/org-subject-menu--title :quit-key "q" :exit t)
  (
   ;; Note: This matches at least one of the :groups in \`jnf/org-roam-capture-subjects-plist'
   "Personal / Public"
   ()
   ;; Note: This matches at least one of the :groups in \`jnf/org-roam-capture-subjects-plist'
   "Projects"
   ()
   "Org Mode"
   (("@" (lambda ()
           (interactive)
           (find-file (file-truename (plist-get (plist-get jnf/org-roam-capture-subjects-plist :all) :path-to-todo))))
     "Todo…")
    ("+" jnf/org-roam--all--capture     "Capture…")
    ("!" jnf/org-roam--all--node-insert " ├─ Insert…")
    ("?" jnf/org-roam--all--node-find   " └─ Find…")
    ("/" org-roam-buffer-toggle         "Toggle Buffer")
    ("#" jnf/toggle-roam-subject-filter "Toggle Default Filter")
    )))

The jnf/org-subject-menu–all frames out the menu structure. The menu has three columns: “Personal / Public”, “Projects”, and “Org Mode”. The “Personal / Public” and “Projects” are the two named groups I assigned each subject in the jnf/org-roam-capture-subjects-plist.

In the above implementation, they start as empty lists. But as we move down the implementation, we’ll append the subjects to those empty lists.

The Macro That Populates the Hydra Menu

Now we get to the create-org-roam-subject-fns-for macro that does the heavy lifting.

Macro create-org-roam-subject-fns-for impelementation.

(cl-defmacro create-org-roam-subject-fns-for (subject
                                              &key
                                              (subjects-plist jnf/org-roam-capture-subjects-plist))
  "Define the org roam SUBJECT functions and create & update hydra menus.

The functions are wrappers for `org-roam-capture’, `org-roam-node-find’, `org-roam-node-insert’, and `find-file'.

Create a subject specific `pretty-define-hydra’ and append to the `jnf/org-subject-menu–all’ hydra via the `pretty-define-hydra+' macro.

Fetch the given SUBJECT from the given SUBJECTS-PLIST." (let* ((subject-plist (plist-get subjects-plist subject)) (subject-as-symbol subject) (subject-title (plist-get subject-plist :title)) (subject-name (plist-get subject-plist :name))

     ;; For todo related antics
     (todo-fn-name (intern (concat "jnf/find-file--" subject-name "--todo")))
     (path-to-todo (plist-get subject-plist :path-to-todo))
     (todo-docstring (concat "Find the todo file for " subject-name " subject."))

     ;; For hydra menu related antics
     (hydra-fn-name (intern (concat "jnf/org-subject-menu--" subject-name)))
     (hydra-menu-title (concat subject-title " Subject Menu"))
     (hydra-todo-title (concat subject-title " Todo…"))
     (hydra-group (plist-get subject-plist :group))
     (hydra-prefix (plist-get subject-plist :prefix))
     (hydra-kbd-prefix-todo    (concat hydra-prefix " @"))
     (hydra-kbd-prefix-capture (concat hydra-prefix " +"))
     (hydra-kbd-prefix-insert  (concat hydra-prefix " !"))
     (hydra-kbd-prefix-find    (concat hydra-prefix " ?"))

     ;; For \`org-roam-capture' related antics
     (capture-fn-name (intern (concat "jnf/org-roam--" subject-name "--capture")))
     (capture-docstring (concat "As \`org-roam-capture' but scoped to " subject-name
                        ".\n\nArguments GOTO and KEYS see \`org-capture'."))

     ;; For \`org-roam-insert-node' related antics
     (insert-fn-name (intern (concat "jnf/org-roam--" subject-name "--node-insert")))
     (insert-docstring (concat "As \`org-roam-insert-node' but scoped to " subject-name " subject."))

     ;; For \`org-roam-find-node' related antics
     (find-fn-name (intern (concat "jnf/org-roam--" subject-name "--node-find")))
     (find-docstring (concat "As \`org-roam-find-node' but scoped to "
                        subject-name " subject."
                        "\n\nArguments INITIAL-INPUT and OTHER-WINDOW are from \`org-roam-find-mode'."))
     )
\`(progn
   (defun ,todo-fn-name ()
     ,todo-docstring
     (interactive)
     (find-file (file-truename ,path-to-todo)))

   (defun ,capture-fn-name (&optional goto keys)
     ,capture-docstring
     (interactive "P")
     (org-roam-capture goto
                       keys
                       :filter-fn (lambda (node) (-contains-p (org-roam-node-tags node) ,subject-name))
                       :templates (jnf/org-roam-templates-for-subject ,subject-as-symbol)))
   (defun ,insert-fn-name ()
     ,insert-docstring
     (interactive)
     (org-roam-node-insert (lambda (node) (-contains-p (org-roam-node-tags node) ,subject-name))
                           :templates (jnf/org-roam-templates-for-subject ,subject-as-symbol)))

   (defun ,find-fn-name (&optional other-window initial-input)
     ,find-docstring
     (interactive current-prefix-arg)
     (org-roam-node-find other-window
                         initial-input
                         (lambda (node) (-contains-p (org-roam-node-tags node) ,subject-name))
                         :templates (jnf/org-roam-templates-for-subject ,subject-as-symbol)))

   ;; Create a hydra menu for the given subject
   (pretty-hydra-define ,hydra-fn-name (:foreign-keys warn :title jnf/org-subject-menu--title :quit-key "q" :exit t)
     (
      ,hydra-menu-title
      (
       ("@" ,todo-fn-name        ,hydra-todo-title)
       ("+" ,capture-fn-name     " ├─ Capture…")
       ("!" ,insert-fn-name      " ├─ Insert…")
       ("?" ,find-fn-name        " └─ Find…")
       ("/" org-roam-buffer-toggle            "Toggle Buffer")
       ("#" jnf/toggle-roam-subject-filter    "Toggle Filter…")
       )))

   ;; Append the following menu items to the \`jnf/org-subject-menu--all'
   (pretty-hydra-define+ jnf/org-subject-menu--all()
     (,hydra-group
      (
       (,hydra-kbd-prefix-todo    ,todo-fn-name    ,hydra-todo-title)
       (,hydra-kbd-prefix-capture ,capture-fn-name " ├─ Capture…")
       (,hydra-kbd-prefix-insert  ,insert-fn-name  " ├─ Insert…")
       (,hydra-kbd-prefix-find    ,find-fn-name    " └─ Find…")
       )))
   )))

The create-org-roam-subject-fns-for macro does six things for the given subject:

  1. Creates a function to find-file of the subject’s todo.
  2. Creates a subject specific capture function that wraps org-roam-capture.
  3. Creates a subject specific insert function that wraps org-roam-node-insert.
  4. Creates a subject specific find function that wraps org-roam-node-find.
  5. Uses pretty-hydra-define to create a subject specific menu.
  6. Uses pretty-hydra-define+ to append menu items to the jnf/org-subject-menu–all menu.

Calling the Macro to Populate the Menu

I then call the create-org-roam-subject-fns-for macro for each of the subjects, except for the :all subject.


(create-org-roam-subject-fns-for :personal)
(create-org-roam-subject-fns-for :public)
(create-org-roam-subject-fns-for :hesburgh-libraries)
(create-org-roam-subject-fns-for :jf-consulting)
(create-org-roam-subject-fns-for :thel-sector)

The Function and Aliases that Allow for Setting the Subject

Because I didn’t call the create-org-roam-subject-fns-for macro for the :all subject, I create some aliases.


(defalias 'jnf/org-roam--all--node-insert 'org-roam-node-insert)
(defalias 'jnf/org-roam--all--node-find 'org-roam-node-find)
(defalias 'jnf/org-roam--all--capture 'org-roam-capture)

In creating these aliases, I reduce the need for complicated logic switching in the jnf/toggle-roam-subject-filter function; this function allows me to toggle the current Org-Roam subject.

Function jnf/toggle-roam-subject-filter implementation

(defun jnf/toggle-roam-subject-filter (subject)
  "Prompt for a SUBJECT, then toggle the 's-i' kbd to filter for that subject."
  (interactive (list
                (completing-read
                 "Project: " (jnf/subject-list-for-completing-read))))
  (global-set-key
   ;; Command + Control + i
   (kbd "s-TAB")
   (intern (concat "jnf/org-roam--" subject "--node-insert")))
  (global-set-key
   (kbd "C-s-c")
   (intern (concat "jnf/org-roam--" subject "--capture")))
  (global-set-key
   (kbd "C-s-f")
   (intern (concat "jnf/org-roam--" subject "--node-find")))
  (global-set-key
   (kbd "s-i")
   (intern (concat "jnf/org-roam--" subject "--node-insert")))
  (global-set-key
   (kbd "C-c i")
   (intern (concat "jnf/org-subject-menu--" subject "/body"))))  (global-set-key
   (kbd "C-c i")
   (intern (concat "jnf/org-subject-menu--" project "/body"))))

The jnf/toggle-roam-subject-filter function once had a hard-coded list of , but I extracted the jnf/subject-list-for-completing-read function to leverage the jnf/org-roam-capture-subjects-plist variable.

Function jnf/subject-list-for-completing-read implementation

(cl-defun jnf/subject-list-for-completing-read (&key
                                                (subjects-plist
                                                 jnf/org-roam-capture-subjects-plist))
  "Create a list from the SUBJECTS-PLIST for completing read.

The form should be ‘(("all" 1) ("hesburgh-libraries" 2))." ;; Skipping the even entries as those are the “keys” for the plist, ;; the odds are the values. (-non-nil (seq-map-indexed (lambda (subject index) (when (oddp index) (list (plist-get subject :name) index))) subjects-plist)))

Loading the Org Roam Package

With all of that pre-amble, I finally load the Org-Roam package.


(use-package org-roam
  :straight t
  :custom
  (org-roam-directory (file-truename "~/git/org"))
  ;; Set more spaces for tags; As much as I prefer the old format,
  ;; this is the new path forward.
  (org-roam-node-display-template "${title:*} ${tags:40}")
  (org-roam-capture-templates (jnf/org-roam-templates-for-subject :all))
  :init
  (add-to-list 'display-buffer-alist
               '("\\*org-roam\\#"
                 (display-buffer-in-side-window)
                 (side . right)
                 (slot . 0)
                 (window-width . 0.33)
                 (window-parameters . ((no-other-window . t)
                                       (no-delete-other-windows . t)))))

  (setq org-roam-v2-ack t)
  (org-roam-db-autosync-mode)
  ;; Configure the "all" subject key map
  (jnf/toggle-roam-subject-filter "all"))

In loading the Org-Roam package, I use the jnf/org-roam-templates-for-subject function to ensure that the capture templates contain “all” of the expected templates.

I also use the jnf/toggle-roam-subject-filter function to build the initial keymap for the “all” subject.

Conclusion

I hope it’s been helpful walking through the what and the how of implementing subject based contexts for Org-Roam.

The process of refactoring towards the create-org-roam-subject-fns-for macro helped me better think through the composition of the menus. In the early stages, I had 1 macro per function definition, but moved to the `(progn) declaration to chain together the creation of several functions.

-1:-- Diving into the Implementation of Subject Menus for Org Roam (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 23, 2021 01:37 PM

Jeremy Friesen: Ever Further Refinements of Org Roam Usage

Leveraging Some Org Roam Version 2 Changes

update: In Diving into the Implementation of Subject Menus for Org Roam, I wrote about the implementation details for the following post.

Earlier I wrote about Adding Hydra Menu for Org Roam Lookup in Emacs and then Revisiting Hydra Menu for Org Roam Lookup in Emacs. I wrote those when I was using Org-Roam 📖 version 1. The release of version 2 of Org-Roam broke that setup. But the breaking changes are well worth it!

Let’s dive into my new Org-Roam menu:

The Org Subject Menu. I invoke jnf/org-subject-menu--all/body via the keybinding C-c i.

Refer to Table #226 below for the description of heavy text-based image
Table 226: Textual Representation of Org Subject Menu
Key CombinationCommand
p tOpen Personal Todo File
p cCapture Personal
p iInsert Personal
p fFind Personal
u cCapture Public
u iInsert Public
u fFind Public
h tOpen Hesburgh Libraries Todo File
h cCapture Hesburgh Libraries
h iInsert Hesburgh Libraries
h fFind Hesburgh Libraries
t cCapture Thel Sector
t iInsert Thel Sector
t fFind Thel Sector
cCapture
iInsert
fFind
/Toggle Org Roam Side Buffer
#Toggle Default Filter

In the default menu, there’s duplication based on subject (e.g., Personal, Public, Hesburgh Libraries, and Thel Sector).

Let’s go over the basic commands:

  1. Capture
  2. Insert
  3. Find
  4. Subject Todo
  5. Toggle Buffer
  6. Toggle Default Filter

The Capture, Insert, and Find are three of the core functions of org-roam; I map org-roam-captureto C-s-c, org-roam-node-insert to C-s-i, and org-roam-node-find to C-s-f.

When you Capture something, you find or create a new node title. If you’re creating a new node, you select your template. You then start writing down your note. When you finish the capture, the buffer closes and you’re back to the original context in which you launched the capture.

When you Insert something, it’s like Capture, except when you finish writing, org-roam inserts a link to your node in the original context in which you launched the capture.

When you Find something, you open a buffer for the found node.

In the above Org Subject Menu, there’s a Capture, Insert, and Find for each subject. Each of those subjects are configured with a set of filters and templates appropriate for the subject. More on that later.

The Subject Todo is my way of partitioning todo lists. Each subject location it’s own todo.org that I maintain.

The Toggle Buffer calls org-roam-buffer-toggle, which toggles the backlinks buffer. When the backlinks buffer is open, and I’m on an org-roam node, I can see the list of nodes that link to the current org-roam node.

The Toggle Default Filter allows me to narrow my Org-Roam activity to a single subject. What does that mean?

The prompt area for toggling the default filter; the subjects are: all, hesburgh-libraries, personal, public, and thel-sector.

An emacs minibuffer with five entries: all, hesburgh-libraries, personal, public, and thel-sector.

When I select the “thel-sector” as the default filter, I re-map C-c i to jnf/org-subject-menu--thel-sector/body. The subject menu looks as follows:

Refer to Table #227 below for the description of heavy text-based image
Table 227: Textual Representation of Org Subject Menu
Key CombinationCommand
cCapture Thel Sector
iInsert Thel Sector
fFind Thel Sector
/Toggle Org Roam Side Buffer
#Toggle Default Filter
Note that I've dropped the t leading key.

In addition, I re-map C-s-c, C-s-i, and C-s-f to functions that automatically narrow the filter and templates to the “thel-sector” subject. So when I’m focusing on a particular subject, I can narrow my keyboard shortcuts to the subject.

Conclusion

This implementation feels much cleaner that my Org-Roam version 1 implementation. There’s both an internal consistency and a few more places to pivot.

I wrote an issue and submitted a pull request to org-roam. The maintainer of Org-Roam merged the pull request, and it’s now part of v2.1.0. The issue includes a lot more detail of the why and the how.

You can checkout this gist for my org-roam configuration. There’s room for improvement, but for now this is working quite well for my needs.

Those diving into the Emacs code will see that the Personal subject currently has two capture templates: a simple template and an encrypted template. It’s relatively simple to add new templates for a given subject.

For example, if I were to do more work in the Thel Sector, I might consider making a capture template for an Non-Player Character 📖, Session Notes, Locations, and Faction Turns.

Similarly, for the Hesburgh Libraries subject I could see templates for Meeting Minutes and Problem Statements. For now, I have a simple template.

I hope this provides some insights into hacking on Emacs 📖 and configuring Org-Roam.

-1:-- Ever Further Refinements of Org Roam Usage (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 22, 2021 08:15 PM

Jeremy Friesen: Using Magit Built-in Functions for Workflow

Improving on a Hacked Together Function Using Magit

I wrote Emacs Script to Review Git Status of Repositories for creating a checklist of repositories to review. Over on /r/emacs, someone provided the following:

Similar to that, one can define magit-repository-directories which is a list of folders for magit to look for git projects - including an optional integer per each representing how deep to search. After you do that you can get a status overview using magit-list-repositories which shows projects name, version, status (untracked, unstaged, staged) and numbers of unpushed/unpulled commits from upstream. Very convenient. Also C-u magit-status lets you jump to one of these repositories using auto-complete.

Curious, I spent a bit of time exploring the Magit 📖 function route, and settled on the following configuration:

(setq magit-repolist-columns
      '(("Name"    25 magit-repolist-column-ident ())
        ("Version" 25 magit-repolist-column-version ())
        ("D"        1 magit-repolist-column-dirty ())
        ("⇣"      3 magit-repolist-column-unpulled-from-upstream
         ((:right-align t)
          (:help-echo "Upstream changes not in branch")))
        ("⇡"        3 magit-repolist-column-unpushed-to-upstream
         ((:right-align t)
          (:help-echo "Local changes not in upstream")))
        ("Path"    99 magit-repolist-column-path ())))
(setq magit-repository-directories
      `(
        ("~/git/takeonrules.github.io/themes/hugo-tufte" . 1)
        ("~/git/takeonrules.github.io/" . 1)
        ("~/git/dotzshrc/" . 1)
        ("~/git/ndlib/sipity" . 1)
        ("~/git/samvera/hyrax" . 1)))

Now when I run M-x magit-list-repositories I get the equivalent buffer:

Table 225: Results of custom M-x magit-list-properties
NameVersionDBranchPath
dotzshrc20210802.2144-g9155fd9 00main~/git/dotzshrc/
sipity20210802.0852-g2ecdaa4 00main~/git/ndlib/sipity
hyrax20210731.1844-g3d92137 00main~/git/samvera/hyrax
hugo-tufte20210731.1839-gfef60e9 00main~/git/takeonrules.github.io/themes/hugo-tufte
takeonrules.github.io20210802.1134-gc84acbf6N00trunk~/git/takeonrules.github.io/

If there’s a non-blank in D column then there’s changes to commit. The column shows me what’s upstream that I don’t have locally. And the column shows me what I have locally that I haven’t pushed upstream.

And from the above buffer, I can quickly open a magit-status buffer to begin commiting changes and synchronizing repositories.

So with that, I can get an overview of all of the relevant repositories and take action accordingly. This supplants jnf/git-data-statuses function.

-1:-- Using Magit Built-in Functions for Workflow (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 03, 2021 01:47 PM

Jeremy Friesen: Emacs Script to Review Git Status of Repositories

Generating a Quasi-TODO List for a Common Mental Model Task

update: After some input on /r/emacs, I wrote Using Magit Built-in Functions for Workflow. Those changes supplant what I’ve written below.

Throughout my day, I work on several different Git 📖 repositories. And sometimes, I can lose track of what all I’ve worked on.

To help with this task, I created the following Emacs 📖 variable and function to let me quickly and methodically check the status of those repositories.

;; This is a truncated list of my projects
(setq jnf/data-dirs
      '(
        "~/git/takeonrules.github.io/themes/hugo-tufte"
        "~/git/takeonrules.github.io/"
        "~/git/dotzshrc/"
        "~/git/ndlib/sipity"
        "~/git/samvera/hyrax"))

(cl-defun jnf/git-data-statuses (&optional (dirs jnf/data-dirs))
  "Review DIRS via `magit'.

By default the DIRS are `jnf/data-dirs'"
  (interactive)
  (message "Review status of local git repos...")
    (dolist (path dirs)
      (if (f-dir-p (file-truename path))
          (magit-status path))))

When I execute the jnf/git-data-statuses command, Emacs opens one Magit 📖 buffer for each of the git repositories in the jnf/data-dirs list. I then work through what I need to do for each git repository.

Below is an example of the magit-status buffer for the ~/git/takeonrules.github.io/ git repository:

Head:     trunk Publishing general update eg. no posts
Rebase:   origin/trunk Publishing general update eg. no posts
Push:     origin/trunk Publishing general update eg. no posts

Untracked files (1)
content/posts/2021/emacs-script-to-review-git-status-of-repositories.md

Unstaged changes (1)
modified   data/glossary.yml

From that buffer, I can perform the various git commands (e.g., stage all changes, commit the changes, push the branch). And when I’m done with that project’s buffer, I close it out and move on to the next project.

-1:-- Emacs Script to Review Git Status of Repositories (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--August 02, 2021 12:59 PM

Jeremy Friesen: Amplifying the Blogosphere (v2021-06-29)

RSS, Procrastination, and Chipping Away at Boundaries

Another Reason Elfeed Is The Best RSS Reader

As you all know, I’m a big fan of Chris Wellons’ Elfeed package for reading my RSS feed. There’s a lot to like. Wellons reimagined what an RSS reader should be and organized it around search. Most of the time, the search is implicit, defaulting to “show me the unread entries for the last 6 months” but you can specify anything you want.

At one point I wroe about Switching from Inoreader to Newsboat for RSS Reader. In my switch to Emacs 📖, I switched to using elfeed. And I love it.

I wrote some scripts for Further Molding Emacs to Reinforce Habits. These scripts help me capture entries in Elfeed and amplify them in my blog. I used the linked script to capture the above blockquote.

Having my feed reader conceptually close to my text editor is analogue to reading a book with pencil in hand; I’m more prone to engage the text I read.

The Gold Hack

Hi! I made a fan hack of The Burning Wheel Gold edition (I don’t own Gold Revised… yet!). It is called The Gold Hack

I made a lot of changes (most of them inspired by Mouse Guard RPG) to reduce the system to 11 pages (+5 if you count the cover, the index of contents, the character sheet, the rules summary, and the credits, license and greetings pages).

I love Burning Wheel Gold, but continue to struggle with Luke Crane’s behavior. The Gold Hack along with Hot Circle

TSR Games has spent the weekend pushing forward with their plan and escalating the fight. The Facebook Group Old School TSR Games has reported the new TSR Games has threatened legal action.

I’m old enough to remember the dying gasps of Tactical Rules Studies (TSR 📖) (the original) when they unleashed as many legal antics as possible. This was the early days of ubiquitous internet. I had heard rumors of a TSR sanctioned site that allowed for people to upload their house rules.

The site only allowed a few people access at a time. I spent many odd hours during my first year of college trying to connect to that site. It’s this odd memory, of green screens and gopher clients. And I once made it into the site, to then scrounge around for all kinds of Advanced Dungeons and Dragons: Second Edition (AD&D 2E 📖) materials. I found a few.

But it was this odd gated space, this promised trove of information. A harbinger of the internet to come. Guarded by the malignant dragon that was TSR. Looks like this incarnation’s doubling down on that legacy. And more.

The psychology of revenge bedtime procrastination

Getting revenge on our daytime life.

The term “bedtime procrastination” was coined in by Dr. Floor Kroese, a behavioral scientist from Utrecht University, and her team. They defined it as “going to bed later than intended while no external circumstances are accountable for doing so.”

I don’t have full control of my calendar and schedule, but when my kids were younger I have memories of this behavior. This is related to burnout, and is an issue with how your employer chooses to treat you. Sure, understand the issues and how to address them. But it’s about time we reimagine coping mechanisms for living in capitalism.

-1:-- Amplifying the Blogosphere (v2021-06-29) (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--June 29, 2021 09:27 PM

Jeremy Friesen: Delving Further into the Why of Emacs

It's Reducing Context Shifting

Over on /r/emacs, one of the community members asked the about Integrated Development Environments (IDEs 📖) with the following post:

I have been using emacs for an year now mostly for Clojure development with little bit of golang and python here and there. When i started using emacs here i convinced myself that

  • it makes me more productive by allowing me to do everything from keyboard
  • multi language support with packages
  • highly customisable with thousands of packages and config.
  • suited for clojure development.
  • org mode

But lately i have been thinking if i was wrong or if my assumptions still holds true, today ides like vs code provide all the above features in a fast modern looking ide.

So does the question of Emacs vs Modern IDEs boils down to asthetic choice between modern vs classic/vintage or is there any real advantage in using Emacs today.

I posted my response regarding Emacs 📖 but figured I’d share that observation here as well:

I use Emacs for 3 reasons:
  1. Coding
  2. Blogging
  3. Note Taking

Is it best for coding? Maybe not. Corporate sponsored IDEs sure seem to provide lots of tooling. (Those same companies deploy a “Embrace, Extend, and Extinguish” strategy regarding FOSS. They’re trying to enclose the commons) .

But, coding is only one concern. I write a lot. For both personal and professional reasons. And in this case Emacs shines like none other; I’ve used Textmate 📖, Sublime Text 📖, Atom text editor 📖, and Visual Studio Code 📖 for those purposes.

But, what I’ve found: using the same tool for all three results in expanding my thinking and ability regarding those three primary topics. When I make one conceptual gain (e.g., think about a function that helps me in my note taking) my other two primary contexts benefit.

And after posting, I continued to think about this.

In the years before adopting Emacs, I would write all kinds of functions for gaming and note taking. I think to my GM::Notepad. After I released that tool, a fellow gamer and tinkerer on the computer gently quipped: “Interesting, were I to have done this I would’ve written that in Emacs.”

I like the concepts of GM::Notepad, but it failed because to use it, I had to run outside of one of my normal contexts. Yes, I often have a terminal window open. However, that context primes me for one off considerations; which is antithetical to the mindset I’m taking while running an Role Playing Game (RPG 📖).

That comment sat with me, not as anything damning, but as a reminder that we each approach problems with different tools.

Where as I’m quite good at Ruby 📖, I’m asking myself, “Why not encode this in Emacs? After all this is where I spend more and more of my digital day.”

From another vantage, by continuing to leverage Emacs, I’m reducing the context switching. And in reducing context switching, I’m creating more space to connect pieces of information to build my personal knowledge.

At this stage, I can’t imagine switching from Emacs to any other software; I’m finding the lessons I’ve learned compound, further expanding my understanding of how I can use Emacs to further my understanding of the games I play, the articles I read, the code I write, and all of the interconnections that emerge.

Emacs helps me get better at doing better with digital information.

-1:-- Delving Further into the Why of Emacs (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--June 13, 2021 12:17 AM

Jeremy Friesen: Amplifying the Blogosphere (v2021-06-11)

Campaigns, Bingo, and Breakinig Changes

Path to org-roam v2

In my opinion, Org-roam 📖 is among the best things that happened to Emacs during the last 5 years. Forget Zettelkasten method, org-roam is a solution for non-hierarchical note-taking regardless of methodology you are worshiping.

And now org-roam is very close to release of V2, the first major redesign affecting both users and authors of libraries extending org-roam. And while PR remains a mere draft, V2 is so amazing and is so much more stable than V1, that my advice is to drop the chains of old org-roam and embrace all the goodness V2 brings.

When I’m running and preparing New Vistas in the Thel Sector I make extensive use of org-roam. It functions like a super-charged personal wiki 📖 that I can extend. And as I wrote in Molding Emacs to Reinforce Habits I Want to Develop, I think migrating my blog to org-roam as the likely end game of my blog management.

Why? Because I already use org-roam as a database for my non-public facing notes. I manually extract things from org-roam that I want to publish on my blog. I’ve made extensive use of Hugo 📖 shortcodes and layouts, so this migration would be non-trival.

Take a look at my Hugo theme if you’re interested. With all of the cross-linking I do, it takes 9 seconds to build my whole site. Before the massive cross-linking effort the build was about 1 second. While 9 seconds is slow, I assume any Emacs 📖 based export from org-roam would be an order or two slower.

Landing in the Lost City of Omu

Using the Bingo Style Experience Points, we started the game writing up the outer ring of the Bingo Board. Bingo chips can be taken off the board to be used as Inspiration.

Okay, I’ll admit, Judd’s Bingo Style Experience Points slipped past my notice.

However, in reading through his Landing in the Lost City of Omu post, I see the tool in action and can’t help but love it.

For the readers, I’m going to transcribe the base bingo board image into an HTML table. See Table 223: Judd Karlman's Bingo Style Experience Points Template for a non-image version of the bingo board. Then I’m going to add the specific bingo game board that is in play for this leg of their Tomb of Annihilation 📖 adventure. See Table 224: Judd Karlman's Bingo Style Experience Points for Exploring the Lost City of Omu.

Table 223: Judd Karlman’s Bingo Style Experience Points Template
TBDTBDTBDTBDTBD
TBDThwarted a rivalDiscovered secret loreMade a dangerous enemyTBD
TBDOvercame a challenge with cunningFree space (counts as completed)Recovered ancient lost treasureTBD
TBDEarned the trust of a wondrous allyAdded a new detail to the mapSaved folk from the tyranny of monstrous beingsTBD
TBDTBDTBDTBDTBD
As a group fill out the TBD squares to reflect the kind of game you want to play.

And below is the table for the Lost City of Omu.

Table 224: Judd Karlman’s Bingo Style Experience Points for Exploring the Lost City of Omu
Pivot the adventure to a new direction…Follow the most simple and direct path…Turn a difficult/tense situation around with humorCut losses and retreat rather than face further losesSave a comrade's life
Help somone achieve a good deathThwarted a rivalDiscovered secret loreMade a dangerous enemySpring a trap on a mighty foe…
Granted mercy to a fallen foeOvercame a challenge with cunningFree space (counts as completed)Recovered ancient lost treasureDo the deed that will inspire the name of the group
Voluntarily leave a prize behindEarned the trust of a wondrous allyAdded a new detail to the mapSaved folk from the tyranny of monstrous beingsRedefined an old relationship
Make a boast and act on itPatch up a feudChoose a different path when violence is the easiest wayBuild a bridgeTrade stories with an enemy

As I think to one of my favorite Tomb of Annihilation sessions— The Stage is Set for an Epic Battle in Omu) and the conclusion King of Feathers Brings the Chomp —I’m mentally checking off what all my group had done.

And I think crafting that bingo board is a great way for the table to telegraph what they want to see in the game. Another way of looking at it is that in the Old School Renaissance (OSR 📖) you often reward Experience Points (XP 📖) for Gold Piece (gp 📖); in later editions you might reward XP for a quest or defeating a monster.

This bingo board encodes those concepts, and as a group facing board helps them see specifically the kinds of things they said they would do to advance their characters. Which, if you blur your eyes a bit, is kind of like Beliefs, Instincts, and Traits (BITs 📖) in Burning Wheel Gold 📖; and definitely like flags from Shadows of Yesterday.

End Game

Experience leads to skill score increase, and ageing leads to attribute score decrease.

A saving throw against ageing is easily portable to other systems.

You do not become invicible, you become weaker. Your power grows, but you’re not holding the dagger. Your head cools, you’re leaving the youthly gardens of the short term. The game has changed.

I yearn to play the long-running campaign. I think to The Great Pendragon Campaign 📖 and the assumption that you start as a young knight, and perhaps they retire, so you play your character’s child.

I see those long-running stories as something that was foundational to my childhood and young-adulthood relationships. A group of 7 or 8 of us would play the same characters for a year or two. And then other campaigns would start with portions of that group and new members. Then we’d mix that up, so that in a group of 15 or so players, each person had played in a few campaigns, but not everyone in all of the campaigns. Together that cohort of 15 shared wonderful stories.

And I suppose I want to revisit that experience, as it created some of my longest lasting and deepest friendships. Perhaps this is the greatest insight into why I write session reports.

-1:-- Amplifying the Blogosphere (v2021-06-11) (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--June 11, 2021 10:08 PM

Jeremy Friesen: Further Molding Emacs to Reinforce Habits

It Ain't Emacs if You Ain't Hacking on Your Config Daily

I wrote about Molding Emacs to Reinforce Habits I Want to Develop. In that post, I outlined how I wrote some functions to accelerate grabbing some text and starting a new blog post.

I refined and further extended those functions. To start, I wrote down the desired behavior.

Desired Behavior

I want a function that will pre-populate an Amplifying post from an elfeed entry.

If there’s an active region (e.g., selected text), I will wrap that region in a blockquote shortcode. The elfeed entry’s title will be the cite parameter and the entry’s url will be the cite_url.

If there’s no active region, add wrap an A-tag in a CITE-tag. The A-tag’s href will be the entry’s url. And the A-tag’s inner html will be the entry’s title.

For Elfeed Mode

I use elfeed for my Rich Site Summary (RSS 📖) feed reader. By implementing the above functional behavior, I’ll more quickly be able to add entries that I read to my Amplifying the Blogosphere series.

The jnf-amplify-elfeed function uses the updated tor-post-amplifying-the-blogosphere (I’ll go into more of that later). Yesterday I decided to map the unused F7 key to tor-post-amplifying-the-blogosphere, so in the elfeed-show-mode-map I decided to over write the global binding but preserve the over-arching functional behavior (e.g. grab the thing and make an entry in today’s amplifying the blogosphere post).

See Github 📖 for the elfeed configuration.

(use-package elfeed
  :straight t
  :after org
  :config
  (setq-default elfeed-search-filter "@2-days-ago +unread ")
  (defun jnf/amplify-elfeed ()
    "Amplify the current `elfeed-show-entry'"
    (interactive)
    (let* ((citeURL (elfeed-entry-link elfeed-show-entry))
           (citeTitle (elfeed-entry-title elfeed-show-entry)))
      (tor-post-amplifying-the-blogosphere citeTitle
                                           :citeTitle citeTitle
                                           :citeURL citeURL)))
  :bind (:map elfeed-search-mode-map
              ("q" . jnf/elfeed-save-db-and-bury))
  :bind (:map elfeed-show-mode-map
             ("" . jnf/amplify-elfeed)
             ("s-7" . jnf/amplify-elfeed)
             ("q" . jnf/elfeed-save-db-and-bury)))

For EWW Mode

In implementing the desired behavior in elfeed, it became trivial to implement this in eww; a text based browser for Emacs 📖.

In cases where the RSS feed is a summary, I often open the elfeed entry in eww. With a small refinement, I created jnf/amplify-eww, a function analogous to jnf/amplify-elfeed.

Similar to the elfeed-show-mode-map, I’m mapping the jnf/amplify-eww to F7. Now, when I’m using eww, I can quickly grab something to add to my Amplifying the Blogosphere series.

See Github for the eww configuration.

(use-package eww
  :straight t
  :config
  (defun jnf/amplify-eww ()
    "Amplify the current `eww-data'"
    (interactive)
    (let* ((citeURL (plist-get eww-data :url))
           (citeTitle (plist-get eww-data :title)))
      (tor-post-amplifying-the-blogosphere citeTitle
                                           :citeTitle citeTitle
                                           :citeURL citeURL)))
  :bind (:map eww-mode-map
              ("U" . eww-up-url)
              ("" . jnf/amplify-eww)
              ("s-7" . jnf/amplify-eww))
  :hook ((eww-mode . jnf/reader-visual)))

For All Other Modes

The tor-post-amplifying-the-blogosphere is independently a useful function. In adding the optional parameters citeTitle and citeURL, I’ve extended it’s usefulness.

As I said earlier, the default function for F7 is to create a record in the Amplifying the Blogosphere series. Other mode-map’s override with a more useful function.

See Github for the tor-post-amplifying-the-blogosphere definition.

(global-set-key (kbd "s-7") 'tor-post-amplifying-the-blogosphere)
(global-set-key (kbd "<f7>") 'tor-post-amplifying-the-blogosphere)

(defun tor-post-amplifying-the-blogosphere (subheading &rest ARGS)
  "Create and visit draft post for amplifying the blogosphere.

If there's an active region, prompt for the `SUBHEADING'.  The file
for the blog post conforms to the path schema of posts for
TakeOnRules.com.

Pull the `citeTitle' and `citeURL' from `ARGS' and pass those
along to the `tor-post---create-or-append'"
  (interactive (list (if (use-region-p)
                         (read-string "Sub-Heading: ")
                       nil)))
  (tor-post---create-or-append
   (format-time-string "Amplifying the Blogosphere (v%Y-%m-%d)")
   :toc "true"
   :subheading subheading
   :series "amplifying-the-blogosphere"
   :tags "response to other blogs"
   :citeTitle (plist-get ARGS :citeTitle)
   :citeURL (plist-get ARGS :citeURL)))

Extended Create or Append Behavior

And here’s the extended function. I’ve added optional parameters for citeURL and citeTitle.

See Github for the tor-post—create-or-append definition.

(defun tor-post---create-or-append (title &rest ARGS)
  "Create or append a post with `TITLE'.

The following `ARGS' are optional:

`:tags' one or more tags, as a list or string, to add to the
        frontmatter.
`:series' the series to set in the frontmatter.
`:toc' whether to include a table of contents in the post.
`:citeTitle' the title of the URL cited (if any)
`:citeURL' the URL cited (if any)
`:subheading' if you have an active region, use this header.

If there's an active region, select that text and place it."
  (let* ((default-directory (concat tor--repository-path
                                    "/content/posts/"
                                    (format-time-string "%Y/")))
         (slug (s-dashed-words title))
         (series (plist-get ARGS :series))
         (citeTitle (plist-get ARGS :citeTitle))
         (citeURL (plist-get ARGS :citeURL))
         (tags (plist-get ARGS :tags))
         (toc (plist-get ARGS :toc))
         (subheading (plist-get ARGS :subheading))
         (fpath (expand-file-name
                 (concat default-directory slug ".md"))))
    ;; If the file does not exist, create the file with the proper
    ;; frontmatter.
    (if (not (file-exists-p fpath))
        (write-region
         (concat "---"
                 "\ndate: " (format-time-string "%Y-%m-%d %H:%M:%S %z")
                 "\ndraft: true"
                 "\nlayout: post"
                 "\nlicenses:\n- all-rights-reserved"
                 "\nslug: " (format "%s" slug)
                 "\ntitle: '" title "'"
                 "\ntype: post"
                 (if series (concat "\nseries: " series))
                 (if toc (concat "\ntoc: true"))
                 (if tags (concat "\ntags:"
                                  (mapconcat
                                   (lambda (tag)
                                     (concat "\n- " tag))
                                   (flatten-tree tags) "")))
                 "\n---\n")
         nil fpath))
    ;; If we have an active region, append that region's content to
    ;; the given file.
    (if (use-region-p)
        (write-region
         (concat
          (if subheading
              (concat "\n## " subheading "\n")
            (if citeTitle (concat "\n## " citeTitle "\n")))
          (if citeURL (concat
                       "\n{{< blockquote cite=\""
                       citeTitle "\" cite_url=\""
                       citeURL "\" >}}\n"))
          (buffer-substring (region-beginning) (region-end))
          (if citeURL "\n{{< /blockquote >}}"))
         nil fpath t)
      ;; Without an active region, if we have a citeURL insert a link
      ;; to it.
      (if citeURL
          (write-region
           (concat
            "\n<cite><a href=\"" citeURL
            "\" class=\"u-url p-name\" rel=\"cite\">"
            (or (citeTitle) (citeURL)) "</a></cite>\n")
           nil fpath t)))
    ;; Finally open that file for editing.
    (find-file fpath)))

Conclusion

With just a bit of work, I expanded the function that I am using for capturing and amplifying posts from the blogosphere.

Along the way, I learned more about plist-get; This is similar to older versions of Ruby 📖 using hashes as named parameters.

And with these modifications, I’m beginning to suspect that I’ll want to use something like jnf/amplify-eww and jnf/amplify-elfeed to quickly add to a blog post that isn’t part of the Amplifying the Blogosphere series.

-1:-- Further Molding Emacs to Reinforce Habits (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--June 09, 2021 02:42 AM