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

Emacs@Habrahabr: [Перевод] Как я пишу на LaTeX в Emacs быстрее, чем от руки



Когда Жиль Кастель задокументировал метод ведения математических записей в Vim в реальном времени, многие захотели добиться тех же скорости и удобства ввода LATEX в Emacs1.

Я работаю на LaTeX весь день: уравнения пишу так же быстро, как на бумаге. И даже быстрее. Почему? За размышлением всегда следует работа пальцев. И это — та ситуация, когда количество переходит в качество. Хотя мои рекомендации — лишь 90 % решения.
Читать дальше →
-1:-- [Перевод] Как я пишу на LaTeX в Emacs быстрее, чем от руки (Post Picard)--L0--C0--December 07, 2021 08:28 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

Emacs@Habrahabr: Почему я написал плагин Ataman и как он может изменить вашу работу с IDE от JetBrains

Ataman – это мой плагин для IDE-шек на базе Intellij. Единственное его предназначение – сделать возможным использование leader key биндингов в моём основном рабочем инструменте – Android Studio.

В этом посте я расскажу, зачем я его сделал и вы удивитесь, как раньше-то без него жили!

А ну, удиви
-1:-- Почему я написал плагин Ataman и как он может изменить вашу работу с IDE от JetBrains (Post Mishkun)--L0--C0--October 04, 2021 07:50 PM

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

Jeremy Friesen: Molding Emacs to Reinforce Habits I Want to Develop

But Also, Don't Go Out and Implement a Bunch of Things

Before I switched to Emacs 📖, I had a Rake 📖 task that I invoked to create new blog posts. I’d hop on the terminal, run the task. That task would both stub out a new blog post and open the task in my editor of choice.

This worked, but the script wasn’t integrated into my text editor. So there was a slight context shift to go from I have an idea for a blog post to I’m starting that blog post.

I decided to rewrite it as a Emacs function. This interactive function, named tor-post-new, prompts for the posts title. I fill out the title, and the function create the file with the appropriate Hugo 📖 front matter and opens that file in a buffer. Later in this post, I share the functions I’ve created.

By moving the function into my text editor, I reduced the friction of creating a new blog post.

, I wrote the first in the Amplifying the Blogosphere. Which got me thinking, I really should create an interactive function to ease writing entries to the Amplifying the Blogosphere series.

So I wrote tor-post-amplifying-the-blogosphere. This function doesn’t prompt for a title, but instead derives the title based on the day. So I extracted a common function so I could have the two interactive functions use the same basic function for creating the file, filling in the front matter, and editing the new file.

And as I was thinking about it, I realized “You know, if I have an active Emacs region, I may as well grab that regions text and copy that into the new buffer.”

Elisp Functions to Ease Creating Posts

update: I’ve updated the code below to provide a bit more utility. I’ve also favored a plist instead of positional parameters.

The Elisp 📖 functions

The following code is available on Github.

(defun tor-post-new (title &optional)
  "Create and visit a new draft blog post for the prompted TITLE.

The file for the blog post conforms to the path schema of posts
for TakeOnRules.com."
  (interactive "sTitle: ")
  (tor-post---create-or-append title))
(defun tor-post-amplifying-the-blogosphere (subheading &optional)
  "Create and visit draft blog post for amplifying the blogosphere.

The file for the blog post conforms to the path schema of posts
for TakeOnRules.com."
  (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 (list "response to other blogs")))

The tor-post--create function is called by both of the above functions. It’s purpose is to encode the logic and procedures for creating a new post based on the given parameters.

(defun tor-post---create-or-append (title &rest ARGS)
  "Create or append a post with TITLE, any ARGS are optional.

ARGS:

`:tags' a list of tags to add to the frontmatter.
`:series' the series to set in the frontmatter.
`:toc' whether to include a table of contents in the post.
`: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))
         (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))
                                   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\n"))
          (buffer-substring (region-beginning) (region-end)))
         nil fpath t))
    ;; Finally open that file for editing.
    (find-file fpath)))
                                  "")))
                "\n---\n")
        nil fpath nil nil t))
    ;; If we have an active region, append that region's content to
    ;; the given file.
    (if (use-region-p)
        (write-region
         (concat
          "\n## YOUR H2 HERE\n\n"
          (buffer-substring (region-beginning) (region-end)))
          nil
          fpath
          t nil nil nil))
    ;; Finally open that file for editing.
    (find-file fpath)))

With the new tor-post-amplifying-the-blogosphere function, and the “grab the active region’s text”, I’m aware of further workflow refinements. I’m also seeing that I’m drawing closer to a possible event horizon in which I shift fully to Org-mode 📖 for blogging. I can almost feel the tug of Org-mode.

Let’s dive into that just a bit.

The goal of the tor-post-amplifying-the-blogosphere function is to make it easy to highlight things I’ve found during the day. And in it’s current implementation, the first thing I find is easier to acknowledge.

For those familiar with Org-mode, I have implemented a naive and rudimentary capture process. I am aware that Org-mode’s current tooling would make easier the Amplifying the Blogosphere process.

However, I’ve only written one post in the Amplifying the Blogosphere, so I’m not about to begin migrating my blog from Hugo to Org-mode. However, knowing that there’s tooling that supports the behavior I want to develop raises the possibility of a migration.

Another way to look at this is to think about the functional desires that I might have. When I’m reading articles, I want an easy way to capture the title, URL, and perhaps a block quote of that article. With that captured information, I want to either create a new for today Amplifying the Blogosphere post or append to an existing one. update: I’ve since updated the code and it now does what I’m describing in the preceding sentence.

Of course, I can mimic quite a bit of this by adding to a copy buffer or Emacs kill ring.

So for now, I’ll proceed with what I have and be mindful of possible future work.

Reflection

An earlier version of myself might have jumped at implementing these speculative changes. But, now in my third decade of professional software development, I see this premature feature development as a questing beast.

In a way, this builds on my observations from Bringing the Whole Toolkit to Problem-Solving; Yes, I might learn something new in pursuing it, but is it actually worth spending that time?

I don’t know, but I do know that slowing down my personal implementation tendencies has always yielded a better solution and conserved my energy to address the important issues. And realistically, I don’t even know if I have a problem that warrants a solution.

-1:-- Molding Emacs to Reinforce Habits I Want to Develop (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--June 07, 2021 03:19 PM

Phil Jackson: Using the CIDER debugger in Evil

When using evil and CIDER together, you might find that rebinding the keys required to control the debugger is a pain. Luckily, you don’t actually have to, you can just enter insert mode instead:

(defun my-cider-debug-toggle-insert-state ()
  (if cider--debug-mode    ;; Checks if you're entering the debugger
      (evil-insert-state)  ;; If so, turn on evil-insert-state
    (evil-normal-state)))  ;; Otherwise, turn on normal-state

(add-hook 'cider--debug-mode-hook 'my-cider-debug-toggle-insert-state)

Borrowed from here.

-1:-- Using the CIDER debugger in Evil (Post)--L0--C0--June 06, 2021 04:00 PM

Phil Jackson: Widescreen emacs and vertical splits

On a widescreen monitor, especially the really wide ones, it’s nice to have only vertical splits. This snippet will give bias to vertical splits:

(defun my-split-window-sensibly (&optional window)
  "replacement `split-window-sensibly' function which prefers
vertical splits"
  (interactive)
  (let ((window (or window (selected-window))))
    (or (and (window-splittable-p window t)
             (with-selected-window window
               (split-window-right)))
        (and (window-splittable-p window)
             (with-selected-window window
               (split-window-below))))))

(setq split-window-preferred-function #'my-split-window-sensibly)
-1:-- Widescreen emacs and vertical splits (Post)--L0--C0--June 06, 2021 12:14 PM

Phil Jackson: Having Smartparens commands work with Evil-mc

As described here, when using evil-mc, it’s very frustrating when a Smartparens command works on only the first of the multiple cursors. Here’s how to fix that:

(dolist (cmd '(sp-up-sexp
               sp-copy-sexp
               sp-down-sexp
               sp-join-sexp
               sp-kill-sexp
               sp-next-sexp
               sp-split-sexp
               sp-wrap-curly
               sp-wrap-round
               sp-raise-sexp
               sp-clone-sexp
               sp-wrap-square
               sp-splice-sexp
               sp-end-of-sexp
               sp-forward-sexp
               sp-backward-sexp
               sp-convolute-sexp
               sp-transpose-sexp
               sp-kill-whole-line
               sp-beginning-of-sexp
               sp-forward-barf-sexp
               sp-forward-slurp-sexp
               sp-backward-barf-sexp
               sp-backward-slurp-sexp
               sp-splice-sexp-killing-forward
               sp-splice-sexp-killing-backward))
    (add-to-list
     'evil-mc-custom-known-commands
     `(,cmd (:default . evil-mc-execute-call))))
-1:-- Having Smartparens commands work with Evil-mc (Post)--L0--C0--June 04, 2021 11:38 PM

Jeremy Friesen: Emacs Function to Rename Hugo Blog Post

Writing a Function to Practice Lisp and Complete an Uncommon Task

As part of writing for my blog, I need to start with a file. I derive the filename from the post’s title. Sometimes, I find myself wanting to change the post’s title.

As a matter of preference, I want the title, the filename, and the slug of the Uniform Resource Locator (URL 📖) to align. I previously wrote an elisp function (tor-post-new) to create a new blog post. That function follows my naming preferences.

Below is that function.

The tor-post-new function

This is the Elisp: dialect of Lisp used in GNU Emacs (Elisp 📖) to create a new TakeOnRules.com blog post.


(defun tor-post-new (title &optional)
  "Create and visit a new draft blog post for the prompted TITLE.

The file for the blog post conforms to the path schema of posts
for TakeOnRules.com."
  (interactive "sTitle: ")

  (let* ((default-directory (concat tor--repository-path
                                   "/content/posts/"
                                   (format-time-string "%Y/")))
         (fpath (concat default-directory (s-dashed-words title) ".md"))
         (slug (s-dashed-words title)))
    (write-region (concat
                   "---"
                   "\ndate: " (format-time-string "%Y-%m-%d %H:%M:%S %z")
                   "\ndraft: true"
                   "\nlayout: post"
                   "\nlicenses:\n- all-rights-reserved"
                   "\nslug: " slug
                   "\ntitle: '" title "'"
                   "\ntype: post"
                   "\n---\n")
                  nil (expand-file-name fpath) nil nil nil t)
    (find-file (expand-file-name fpath))))

With the above, I type M-x tor-post-new and fill in the blog posts title. This creates a configured file (and buffer) for me to start writing a blog post.

Earlier today, in writing Conceptualizing a Process for Where and How to Publish the Thing, I had a different working title. I decided to change the title.

To conform to my preference, I chose to replace the title, slug, and renamed the file. Nothing complicated.

I do this a few times a year, but figured I’d practice my Elisp and write jnf/retitle-tor-content, a function that retitles a blog post. Below is that function.

The jnf/retitle-tor-content function

This is the Elisp to re-title a TakeOnRules.com blog post.


(defun jnf/retitle-tor-content (&optional title)
  "Replace the given buffer's title with the new TITLE.

This function will: replace the content's title, update the slug,
and rename the buffer."
    (interactive "sTitle: ")
    (let* ((metadataTitle (concat "title: '" title "'"))
           (slug (s-dashed-words title))
           (metadataSlug (concat "slug: " slug))
           (filename (buffer-file-name))
           (new-filename (concat (file-name-directory filename)
                                 slug
                                 ".md")))

      ;; Replace the title metadata entry
      (goto-char (point-min))
      (while (search-forward-regexp "^title:.*$" nil t)
        (replace-match metadataTitle))

      ;; Replace the slug metadata entry
      (goto-char (point-min))
      (while (search-forward-regexp "^slug:.*$" nil t)
        (replace-match metadataSlug))

      ;; Need to save before we rename the buffer
      (save-buffer)

      ;; Rename the buffer, accounting for version control
      (cond
       ((vc-backend filename)
        (vc-rename-file filename new-filename))
         (t
          (rename-file filename new-filename t)
          (set-visited-file-name new-filename t t)))

      ;; Report filename change
      (message "Renamed %s -> %s" filename new-filename)))

All told, this took about 45 minutes; I’ll never save that much time from this function. However, I continued to learn more about Emacs (Emacs 📖). I took actions reinforcing that I can extend my text editor to conform to my uses. And I have something to contribute to the larger group of Emacs adopters.

-1:-- Emacs Function to Rename Hugo Blog Post (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 20, 2021 08:33 PM

Jeremy Friesen: A Year or So of Emacs

Swapping out Ivy for Selectrum

Jakub Kadlčík FrostyX.cz wrote A Year with Emacs. on /r/emacs, someone asked How much time you need to spent with Emacs to become more productive?

All of this reminds me that I’m coming upon my first anniversary of adopting Emacs 📖. According to my configuration log entry It looks like is my Emacs anniversary. I have no idea how that was my first “commit” for Emacs. But in the days prior, I was committing changes to configure my Visual Studio Code (VS Code 📖) installation, so we’ll go with . I have long since uninstalled VS Code and Atom text editor (Atom 📖).

Earlier I wrote about Why I Chose Emacs as My New Text Editor, and I’ve changed a few things. Instead of using Ivy/Counsel/Swiper, I’m now using Selectrum/Consult.

Switching from Ivy to Selectrum

During my first year of Emacs, I’ve started following Sacha Chau’s blog. This includes Emacs News. And I’ve joined /r/emacs and /r/planetemacs. A common thread is tips and tricks that people have used to further configure Emacs to their needs or as a challenge/exercise for themselves.

I had initially settled on Ivy and Swiper and Counsel for search enhancements. And I experimented with Helm et al including Helm Swoop)

In , I learned about Selectrum and then Embark.

As I read about Selectrum, I honed in on two sections:

The focus of Selectrum is on providing an enhanced completion UI and compose with other packages which stay within the constraints of the standard Emacs API.

Ivy is a promising alternative to Selectrum. It is described as a minimal alternative to Helm which provides a simpler interface. The problem with Ivy is that its architecture and API have grown organically, and as a result the implementation is complex. Ivy was originally designed to be used as a backend to Swiper, a buffer search package that originally used Helm. When Ivy became a more general-purpose interactive selection package, more and more special cases were added to try to make various commands work properly. As a result, the ivy-read API is complex with around 20 arguments and multiple special cases for particular values. Numerous functions in Ivy, Counsel, and Swiper have special cases hardcoded into them to detect when they’re being called from specific other functions in the other two packages.

What caught my eye is Selectrum’s cleaving close to the Emacs Application Programming Interface (API 📖) and that Ivy has expanded in scope. In my experience this means that Ivy will be harder to maintain/extend/support compared to Selectrum. Note: I’m looking at scopes of 5+ years, as I intend for Emacs to be my editor of choice until I’m no longer using a computer.

That difference prompted me to explore what would it take to switch?

First, I clarified what I wanted. The primary feature was Swiper-like behavior. I use Swiper all the time to orient to the contents of a buffer. I found consult-line to be a great replacement. consult-line comes from the Consult package

Other behaviors that I’ve found useful is Counsel edit mode and wgrep-ag.

With those features in mind, I began my refactor.

I went through my Emacs configuration, and gathered up all of the declarations I had for using Ivy, Swiper, and Counsel. I moved those into a file. Then restarted Emacs. And “everything” worked. I removed the require for that file, and nothing broke on initialization. This gave me enough confidence to say was successful in isolating those packages and packages that depended on them.

You can see that file in in my emacs/jnf-ivy.el file. Also, I did all of this rework with Git (git 📖) commits. Thus, if I broke something, I could rollback.

With Ivy et al isolated, I created emacs/jnf-selectrum.el and started configuring/building the replacement.

At one point, during the replacement, I decided to halt work and go back to Ivy. I wasn’t yet convinced that this puttering on my Emacs config was worthwhile. Later, I picked up the work and completed the cutover.

I tested the replacement by removing the require for jnf-ivy.el and adding a require for jnf-selectrum.el. With some poking and prodding, I was mostly up and running. And to verify the isolation, I removed the require for jnf-selectrum.el and nothing broke during Emacs initialization. So I had successfully compartmentalized both the Ivy and Selectrum ecosystem.

A few things in the Selectrum / Consult ecosystem that I love:

Preview of Buffers

For most of the buffer oriented features (e.g., consult-bookmark, consult-buffer, etc.), when the mini-buffer opens and you select a different entry, the original window shows the file. A nice helper when you’re looking to change context.

Embark Export

The embark-export function, similar to Ivy’s occur, opens a new buffer with the current mini-buffer results. From there I can use wgrep to edit lines from other files. All within the original mini-buffer context.

Consult Yank

The consult-yank function is one that’s on probation. I might like it, but am continuing to try it out. The consult-yank function wraps yank but provides a minibuffer preview of what you would be yanking back into the buffer. The hiccup, is you need to confirm the consult-yank.

As a long-time user of Jumpcut, I like the ability to preview what I’m about to paste, and be able to select from the history of copy/cut calls.

Further Delves into Elisp

I’ve been programming for years, and have preivouly dabbled with Lisp 📖. It’s different, but in the last month, Elisp 📖 started feeling quite natural.

I’ve written about Adding Hydra Menu for Org Roam Lookup in Emacs, and extended Emacs to conform to my note taking, blogging, software development, Rich Site Summary (RSS 📖) reader, and PDF 📖 annotator. These days, I look at “What am I doing outside of Emacs that I might be able to better incorporate into Emacs?”

I wrote an advice-add function that mirrored a feature I found in Helm Swoop. Namely, when you initiate consult-line, if you’ve highlighted text, it will use that for the initial filter. Otherwise, it will use the word at point.

(advice-add #'consult-line
            :around
            #'jnf/consult-line
            '((name . "wrapper")))

(defun jnf/consult-line (consult-line-function &rest rest)
  "Advising function around `CONSULT-LINE-FUNCTION'.

When there's an active region, use that as the first parameter
for `CONSULT-LINE-FUNCTION'.  Otherwise, use the current word as
the first parameter.  This function handles the `REST' of the
parameters."
  (interactive)
  (if (use-region-p)
      (apply consult-line-function
        (buffer-substring (region-beginning) (region-end)) rest)
      (apply consult-line-function
        (thing-at-point 'word) rest)))

Conclusion

With my Emacs config are little changes I’ve made to make Emacs better fit my approaches. I’m also

Emacs feels like an ecosystem that is defiant of corporate Embrace, Extend, and Extingush approaches. I look to The Open-Source Software bubble that is and the blogging bubble that was, and am thankful I continue blogging and use a Free Open Source Software (FOSS 📖) text editor that’s not actively a part of Google, Microsoft, Facebook, Amazon, nor Apple’s full-blown extraction efforts.

My plan with Emacs is to engage with the community of practioners and provide what help I can. I’m not an expert, but I’m more experienced than some of us using Emacs.

update

I had nor have no current problems with Ivy/Swiper/Counsel (ISC). I think they’re great tools. What I found is that Selectrum/Consult/Embark (SCE) covered how I used ISC. There may be additional functionality of ISC that I’m missing out on, or don’t know exist.

So, given that my desired feature set exists in ISC and SCE, then I started looking at package size and surface area of functionality. In my experience, the larger that surface area the more effort will be required to maintain it. Which isn’t to say that that effort won’t be applied, it’s instead framing that as a risk.

And in taking the time to test and use both, I’m in a better position to be more specific in why I’m using the package.

-1:-- A Year or So of Emacs (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--May 15, 2021 12:06 PM

Jeremy Friesen: Many Small Tools Make Light Work (in Emacs)

Stiching Together YASnippet, Hugo Short Codes, Shell Commands, and YAML Files in Emacs

Recently, I received heart warming words of thanks and encouragement from emsenn.net for my recent Emacs (Emacs 📖) blog posts. See Toggling Keyboard Mapping for Org Roam and Revisiting Hydra Menu for Org Roam Lookup in Emacs.

I want to share a few more bits of tooling I use to assist in my writing. This involves the following:

  • The YASnippet package for Emacs
  • A Hugo (Hugo 📖) shortcode
  • Some Elisp: dialect of Lisp used in GNU Emacs (Elisp 📖)
  • A Yet Another Markup Language (YAML 📖) file
  • The The Silver Searcher (ag 📖), cut (Unix command) (cut 📖), and sort (Unix command) (sort 📖) command-line functions.

Example Walkthrough

Throughout my site you might read the following:

In my editor, I write those those three list items with the following markdown and Hugo shortcode.


* {{< linkToGame "swn" >}}
* {{< linkToSeries "new-vistas-in-the-thel-sector" >}}
* {{< abbr "swn" >}}

Anatomy of a Hugo Shortcode

A Hugo shortcode encodes a bit of rendering logic. It’s useful when multiple pages want to render the same concept but provide different options.

Note: I added line numbers to help discuss this.

Here is my custom linkToGame shortcode
 1  {{- $game := .Get 0 }}
 2  {{- $key := printf "linkToGame-%s" $game }}
 3  {{- $entry := index (where $.Site.Data.glossary "game" "eq" $game) 0 }}
 4  {{- if ($.Page.Scratch.Get $key) }}
 5    <cite>{{- $entry.title }}</cite>
 6  {{- else }}
 7    {{- with $entry.offer }}
 8      <cite><a href="{{- . }}">{{- $entry.title }}</a></cite>
 9    {{- else }}
10      <cite>{{- $entry.title }}</cite>
11    {{- end }}
12  {{- end }}
13  {{- .Page.Scratch.Set $key "t" }}

Let’s walk through each of the 13 lines. The actual code for this is 20 lines long and includes some additional markup. For the purposes of simplifying this post, I’ve shorted the shortcode.

Line 1: {{- $game := .Get 0 }}
Get the first positional parameter for this shortcode. In the above example, $game is the string swn.
Line 2: {{- $key := printf "linkToGame-%s" $game }}
Create a string (e.g. `linkToGame-swn`). Using Hugo's Scratch variable, I'll use this string as a means of knowing if I've previously rendered a link. If I use this shortcode more than once on a page, I only want to render a link the first time.
Line 3: {{- $entry := index (where $.Site.Data.glossary "game" "eq" $game) 0 }}
Lookup up given game in my personal glossary; Choose the first found instance and set that to the `$entry` variable. This leverages Hugo's Data Folder. My glossary contains several hundred concepts. I'll get into that later on the Glossary File.
In Ruby, the Glossary file is an Array of Hashes. In Golang, the Glossary file is a Map of Dictionaries.
Line 4: {{- if ($.Page.Scratch.Get $key) }}
This line of logic asks: "If we've already rendered a link to this game."
Line 5: <cite>{{- $entry.title }}</cite>
Since we've already rendered a link, don't render a link to the game. Instead cite the game by the glossary entry's title.
Line 6: {{- else }}
Else, if we haven't already rendered the link to this game, then do line 7 through 11.
Line 7: {{- with $entry.offer }}
Using Hugo's with function, if the given game's glossary entry has an `offer` attribute, then do line 8.
Line 8: <cite><a href="{{- . }}">{{- $entry.title }}</a></cite>
Render a link to a purchase offer for this game.
Line 9: {{- else }}
Else, if I don't have a URL, do line 10.
Line 10: <cite>{{- $entry.title }}</cite>
I don't have a link for the game, so cite the game's title instead.
Line 11: {{- end }
Close the with statement opened on line 7.
Line 12: {{- end }
Close the if statement opened on line 4.
Line 13: {{- .Page.Scratch.Set $key "t" }}
Record, via the scracth, that we've rendered this game via. Future calls to this shortcode within the same page will now answer line 4 as false.

The Glossary File

The following references YAML notation. The indentation matters, as does the - at the beginning of the first line. In this single Dictionary entry, there are 7 terms: title, key, itemid, offer, tag, game, and abbr. Each term has a single value (e.g. ‘Stars without Number: Revised Edition’, SWN, etc).

In line 3 of the linkToGame shortcode, we fetch the Dictionary (or Hash) that has swn the game key. Below is a glossary entry printed in YAML form.

- title: 'Stars without Number: Revised Edition'
  key: SWN
  itemid: https://www.wikidata.org/wiki/Q67963569
  offer: https://www.drivethrurpg.com/product/226996/Stars-Without-Number-Revised-Edition?affiliate_id=318171
  tag: swn
  game: SWN
  abbr: SWN

update: I extracted the glossary to my Hugo theme repository; Checkout the README for more information.

The additional keys help me maintain consistency reference the same concepts post after post. Examples include:

  • Using consistent abbreviations
  • Linking to disambiguation pages (e.g. Stars without Number has a Wikidata ID of Q67963569)
  • Consistently linking to offer URLs
  • Using the same title

The YASnippet for linkToGame

I use YASnippet to help in my writing. One of the snippets I have is to help with my linkToGame shortcode. Below is that snippet:


# -*- mode: snippet -*-
# contributor : Jeremy Friesen <jeremy@takeonrules.com>
# group: takeonrules
# name: {{< linkToGame >}}
# key: ltg
# --
{{< linkToGame "${1:$$(yas-choose-value (tor-game-list))}" >}}$0
The linkToGame snippet prompting for one of the games

That snippet is available in my blogging context. When I type ltg followed by the TAB key, the ltg expands into the following: {{< linkToGame "" >}}. The cursor positions between the two quote marks, and Emacs prompts me to select from the given list.

I generate that list in Emacs with the function named tor-game-list.

The tor-game-list Function

update: I added the fourth piped shell command (e.g. tr) to the Elisp function. In adding that fourth shell command, I can now account for spaces in my game entries.

Below is the Elisp code to generate the list for the YASnippet.

1 (defun tor-game-list ()
2  "Return a list of games from TakeOnRules.com."
3  (split-string-and-unquote
4   (shell-command-to-string
5    (concat
6     "ag \"game: .*$\" "
7     (f-join tor--repository-path "data/glossary.yml")
8     " -o --nofilename | cut -d \" \" -f 2- | sort" | tr '\n' '~'"))
9    "~"))

Let’s step through the function:

Line 1: (defun tor-game-list ()
Define the function named `tor-game-list`; This function has no input parameters.
Line 2: "Return a list of games from TakeOnRules.com."
A Docstring (Docstring 📖) that describes the function.
Line 3: (split-string-and-unquote
A function that will split the STRING into a list of strings. In this case the STRING is the results of function call on line 4, and the optional separator (e.g. "`~`" is on line 9.)
Line 4: (shell-command-to-string
A function that will execute shell command COMMAND and return its output as a string. In this case the COMMAND is the result of line 5.
Line 5: (concat
A function that will concatenate all the arguments and make the result a string. In this case those arguments are lines 6, the result of line 7's function call, and line 8.
Line 6, 7, 8
The result of lines 6, 7, and 8 is ag "game: .*$" ~/git/takeonrules/data/glossary.yml -o --nofilename | cut -d " " -f 2- | sort | tr '\n' '~'. This is the command then run by line 4.
Line 9 is "~"))
We will split the results of line 4 on the "`~`" character.

The Shell Commands

I want to explain the shell command a bit more. I’ve separated the command into the four salient parts:

  1. ag "game: .*$" ~/git/takeonrules/data/glossary.yml -o ‐‐nofilename
  2. cut -d " " -f 2-"
  3. sort
  4. tr ‘\n’ ‘~'

First is the ag command.

The first parameter (eg. "game: .*$") is a regular expression. The regular expressions will find lines that have the phrase game: followed by a space, and any sequence of characters.

Looking back at my glossary file, this regular expression will only select the line game: swn.

The second parameter is the path to my glossary. The third parameter (eg. -o) says to only print the matching portions. The fourth parameter (eg. --nofilename) says to skip printing what file had the match.

, here's the results of that ag command.
game: awda
game: bwg
game: diaspora
game: dragonknights
game: dcc
game: dw
game: 5e
game: ll
game: lh
game: mhrpg
game: shsrd
game: sfad
game: swn
game: torchbearer
game: traveller
game: wwn

The second command (e.g. cut -d " " -f 2-) treats the entries like table. The -d " " switch identifies the column separator as an empty space (e.g. SPACE). The -f 2- switch tells the command to pick the second column and everything after. This is important if a game in my glossary were multiple words separated by spaces.

See the results of running the aforementioned cut command with the the above ag command output.
awda
bwg
diaspora
dragonknights
dcc
dw
5e
ll
lh
mhrpg
shsrd
sfad
swn
torchbearer
traveller
wwn

The third command (e.g. sort) alphabetizes the entries.

The fourth command (e.g. tr (Unix command) (tr 📖)) will take the multi-line result, which is split by the \n character, and join those lines by with the ~ character.

Conclusion

I hope this rather extensive walkthrough highlights how to use different functional pieces to improve your writing workflows.

For myself, I write to learn and explore. In the process of writing, I’m also

-1:-- Many Small Tools Make Light Work (in Emacs) (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 20, 2020 11:10 PM

Jeremy Friesen: Directory Structure for my Org Instance

Sharing Some Personal Documentation

This blog post is a repurposing of personal documentation for my internal note taking structure using org-mode and org-roam.

This blog post details the note taking structure that works for me; In part because I use the same tool for writing software as I do for writing notes and blog posts. This means most of my written output goes through the same ecosystem, and benefits from the same tooling.

I found the process of writing the intial documentation helpful; It provided concretion to my own organizational structure.

Before I get started, I wanted to provide a diagram of my org-directory:

.
├── fleeting
├── permanent
│   ├── bibliographies
│   ├── cards
│   └── letters
└── projects
    ├── ardu
    ├── hesburgh-libraries
    ├── samvera
    └── thel-sector

I also have a root level agenda.org file that I use for my daily todo items.

In reading “How to Take Smart Notes” by Sonke Ahrens, the author discusses three types of notes:

  • Fleeting
  • Permanent
  • Project

I’ve expanded this to reflect my mode of thinking and operation.

Organizational Concepts (for Me)

This blog post provides some additional conceptual framing for a previously written post about using Emacs and the Hydra package to create a menu for my Org concepts.

Let’s look at the concepts.

Fleeting

Fleeting notes are momentary “flashes”. They may be more than that, but their purpose is to capture the moment. There is an assumed process that will review fleeting notes within two days or so to see if they merit moving to something permanent.

A useful analogy is that these are my sticky notes. I put all of my sticky notes in the same place and can review them at my next convenience.

Permanent

Permanent notes are of three varieties:

Bibliographies

Bibliographies are for cited works. I’ve adopted a "Title" by author format for the “root” object; However, nothing limits me from having multiple bibliographic records for the same book/article (perhaps a citation or passage). I have not went through the rigors of grad school, and my degree is in applied mathematics and computer science, so citations are something that I’ve organically developed.

I haven’t settled on what could be bibliographic citations or cards as it relates to chapters or sub-sections or concepts of a book. For example I filed the chapter “Six Steps to Successful Writing: Separate and Interlocking Tasks (Sonke Ahrens)” in ./permanent/bibliographies yet I filed the sub-sections (e.g. “Become an Expert Instead of a Planner (Sonke Ahrens)") in ./permanent/cards, as I’m discussing those concepts a bit more.

Cards

A Card my own creation, but highlights that its something beyond a fleeing note. The card is where you flesh out ideas, and continue to inter-relate those ideas. The moniker card pays homage to Zettelkasten.

I’ve assigned a sorting suffix to all ./permanent/cards. The schema for the sorting suffix is set using the following “pattern”: 000aa00aa00aa00­­­slug_with_many_words.org.

Top-level concepts, as they arrive in the “slip-box”, will have a three digit prefix, followed by ­­­ and the slug. The three digit prefix will be “1” greater than the highest prefix.

Concepts “nested” within will then have a prefix of 000aa the 000 will match the containing concept and the aa will increment based on the next available slug. If we had 001aa and incremented we’d have 001ab. I wrote create-index-for-permanent-cards to use the above documented schema. I also have the script sort-unsorted for sorting an unsorted card.

Letters

Letters are the weird things for which I’ve felt are important to keep, but are conceptual terminals.

Projects

Project notes typically exist in a single project, and when the project completes may be “disposable”.

Example projects are my Thel Sector campaign; I use these notes to write about the New Vistas in the Thel Sector series.

I also have a project for my work on Samvera. While our community wiki was down for migration maintainence, I wrote our Tech Call notes as a note in the Samvera project.

Later, I needed to add those notes back to the wiki. And since I wrote the notes in the Samvera project, I likely won’t lose them if I forget to follow-up on that task.

Org Roam’s find-file mini-buffer. The words in parantheses are tags.

I configure my org-roam-tag-sources to use explicit tags and implicit tags from the directories. (setq org-roam-tag-sources '(prop all-directories)) Then, when I use org-roam' find file or insert function, the tags are part of the search index and display prompt.

If I want a note to span multiple projects, I can add the project name as a tag to that note.

Automation

I created two scripts to help manage my directory structure:

org-file-link-checker
to help audit the health of my Org file links. I haven't used this much as Org Roam provides some built-in functionality when renaming documents in the UI.
org-file-mover
Helps move files within the above structure. It implements and negotiates the card prefix algorithm.

These scripts helped “Reduce the Number of Decisions”—Sonke Ahrens.

Conclusion

I’ve been writing quite a bit about my Emacs (Emacs 📖). tooling. I hope sharing the organizational structure that works for me provides some insights in how to configure Emacs (or other text editors) to work for you.

-1:-- Directory Structure for my Org Instance (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 18, 2020 03:41 PM

Jeremy Friesen: Toggling Keyboard Mapping for Org Roam

Dynamically Changing Keyboard Shortcuts to Match Working States

I previously wrote about creating a Hydra menu for Org Roam projects. I’ve been using this for the past 10 days, and it’s working great.

However, I noticed one nussance. If I’m focusing on a project, I am usually only using one of those menu items.

For example, when I spent writing up adventure notes for a new Worlds without Number 📖 game, I only used the “World of Ardu” menu option.

What I wanted was a quick means to temporarily override the keyboard shortcut that invoked the Hydra menu. The override would then filter the org-roam-insert for the specific project.

In other words, when I’m writing notes for my New Vistas in the Thel Sector game, I want the search and insert function of org-roam narrowed to only the Thel Sector project. This narrowed scope removes a keystroke, keeps my roam-roam interactions topical, and reinforces one of my goto key combinations when writing.

Some Code

Elisp function to toggle what Cmd+i invokes.

See the relevant Github file for more context.

(defun jnf-toggle-roam-project-filter (project)
  "Prompt for a PROJECT, then toggle the `s-i' kbd
  to filter for that project."
  (interactive (list
                (completing-read
                 "Project: " '((":all" 1)
                               ("ardu" 2)
                               ("hesburgh-libraries" 3)
                               ("permanent-bibliographies" 4)
                               ("permanent-cards" 5)
                               ("samvera" 6)
                               ("thel-sector" 7)))))
  (if (string= project ":all")
      (global-set-key (kbd "s-i")
        'jnf-org-subject-menu/body)
      (global-set-key (kbd "s-i")
        (intern (concat
                   "org-roam-insert--filter-with--"
                   project)))))

Emacs Gives You the Tools to Help You Out

This solution helps me move from general writing situations into project specific writing modes. It also hightlights the dynamism of Emacs (Emacs 📖).

While I’m writing code, prose, notes, or documentation, if I think of something that might help me out, I take a quick note. Later, I sit with the problem a bit.

I think through if it’s something I want, and how it would might be useful. I then work out a possible approach and write up some Elisp: dialect of Lisp used in GNU Emacs (Elisp 📖) to help out.

The goal of these tweaks is to help keep me in a flow state. And Emacs provides ample means for tweaking and helping me operate closer to my thoughts.

-1:-- Toggling Keyboard Mapping for Org Roam (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 18, 2020 02:21 PM

Jeremy Friesen: Revisiting Hydra Menu for Org Roam Lookup in Emacs

A General Solution (Thusfar)

Previously, I wrote about Adding Hydra Menu for Org Roam Lookup in Emacs. Since then, I’ve explored the solution.

update: Over on org-roams Slack channel, @nobiot provided some clarity on exploring the solution. Neither @nobiot nor myself are experienced Elisp developers.

As a recap, I wanted two key behaviors from Org Roam.

First, I wanted a way to narrow the scope of my initial search when finding a file. I have two Role Playing Game (RPG 📖) campaigns, and want a quick way to limit the search to a given campaign. Here’s that initial solution.

Second, I wanted a way to narrow the scope of linking to a file. When I’m running a game via Zoom, I’m often taking notes while playing. When I create a new concept (e.g. an Non-Player Character (NPC 📖) or location) I want to create an org-roam note for that concept. I also want the lookup to have a similar filter.

Working Through the Solution

A Hydra Menu for Org Roam. This Hydra menu organizes the subjects.

Here’s what I have. There are four general areas:

Permanent
Non-project information; Concepts that span work and play.
RPGs
The campaigns I'm running or building.
Work
Stuff that pays the bills.
General Org
Other org-mode utilities.

Within each area, I have a few concepts. Let’s dive into the RPG area.

I have four menu options, two options for each of the two subjects: The World of Ardu and the Thel Sector. Each of these subjects have two actions; Thus four menu options.

The find file and insert prompt for the "Ardu" subject. For those following along, both find file and insert now use the filter-fn parameter of the underlying org-roam-insert and org-roam-find-file. Also, by using the filter-fn parameter, the search text does not include the project.

The Find action launches a prompt to search the scoped concern. That is “Thel Sector Find” will open a prompt limiting the file search to the Thel Sector files. By convention the key for each of these actions is the upper case of the related insert action.

The insert action is where the magic lives. When I select that action, I open a similar file search as above. When I select a file, it creates a link in the originating buffer. If I don’t find a matching file, I’m prompted to create a quick note.

update: Based on my directory structure fixed the permanent,bibliographies and permanent,cards to account for the comma separation of the roam tags.

Emacs Code for Org Subject Menu

You can find also find this code on Github. Below is that code.

(defun xah-filter-list (@predicate @sequence)
  "Return a new list such that @PREDICATE is true on all members of @SEQUENCE.
URL `http://ergoemacs.org/emacs/elisp_filter_list.html'
Version 2016-07-18"
  (delete
   "e3824ad41f2ec1ed"
   (mapcar
    (lambda ($x)
      (if (funcall @predicate $x)
          $x
        "e3824ad41f2ec1ed" ))
    @sequence)))

(defmacro org-roam-inserter-fn (project)
  "Define a function to wrap the `org-roam-inser' with a filter for the given PROJECT."
  (let* ((fn-name (intern (concat "org-roam-insert--filter-with--"
                                  (replace-regexp-in-string "\\W+" "-" project))))
         (docstring (concat "Insert an `org-roam' file for: " project)))
    `(defun ,fn-name (&optional lowercase completions description link-type)
       ,docstring
       (interactive "P")
       (let* ((filter (lambda(completions) (xah-filter-list
                                            (lambda(item) (string-match-p (concat "\\W" ,project "\\W") (first item)))
                                            completions))))
         (org-roam-insert lowercase completions filter description link-type)))))

(defmacro go-roam-find-file-project-fn (project)
  "Define a function to find an `org-roam' file within the given PROJECT."
  (let* ((fn-name (intern (concat "org-roam-find-file--" (replace-regexp-in-string "\\W+" "-" project))))
         (docstring (concat "Find an `org-roam' file for: " project)))
    `(defun ,fn-name (&optional initial-prompt completions)
       ,docstring
       (interactive)
       (let* ((filter (lambda(completions) (xah-filter-list
                                            (lambda(item) (string-match-p (concat "\\W" ,project "\\W") (first item)))
                                            completions))))
         (org-roam-find-file initial-prompt completions filter)))))

(go-roam-find-file-project-fn "thel-sector")
(go-roam-find-file-project-fn "ardu")
(go-roam-find-file-project-fn "permanent,bibliographies")
(go-roam-find-file-project-fn "permanent,cards")
(go-roam-find-file-project-fn "hesburgh-libraries")
(go-roam-find-file-project-fn "samvera")
(org-roam-inserter-fn "thel-sector")
(org-roam-inserter-fn "ardu")
(org-roam-inserter-fn "permanent,bibliographies")
(org-roam-inserter-fn "permanent,cards")
(org-roam-inserter-fn "hesburgh-libraries")
(org-roam-inserter-fn "samvera")

(defvar jnf-org-subject-menu--title (with-faicon "book" "Org Subject Menu" 1 -0.05))
(pretty-hydra-define jnf-org-subject-menu (:foreign-keys warn :title jnf-org-subject-menu--title :quit-key "q")
  (
   "Permanent"
   (("b" org-roam-insert--filter-with--permanent-bibliographies "Bibliography")
    ("B" org-roam-find-file--permanent-bibliographies " └─ Find")
    ("c" org-roam-insert--filter-with--permanent-cards "Card")
    ("C" org-roam-find-file--permanent-cards " └─ Find"))
   "RPGs"
   (("a" org-roam-insert--filter-with--ardu "Ardu, World of")
    ("A" org-roam-find-file--ardu " └─ Find")
    ("t" org-roam-insert--filter-with--thel-sector "Thel Sector")
    ("T" org-roam-find-file--thel-sector " └─ Find"))
   "Work"
   (("h" org-roam-insert--filter-with--hesburgh-libraries "Hesburgh Libraries")
    ("H" org-roam-find-file--hesburgh-libraries " └─ Find")
    ("s" org-roam-insert--filter-with--samvera "Samvera")
    ("S" org-roam-find-file--samvera " └─ Find"))
   "General Org"
   (("i" org-roam-insert "Insert Unfiltered")
    ("I" org-roam-find-file " └─ Find")
    ("O" gorg "Agenda")
    ("R" org-roam-jump-to-index "Roam Index"))
))

(global-set-key (kbd "s-1") 'jnf-org-subject-menu/body) ;; Deprecated
(global-set-key (kbd "s-i") 'jnf-org-subject-menu/body)

update

If you dig into the code, you’ll see quite a bit of duplication. I spent just a bit of time trying to remove the duplication.

The one that sticks out most is the filter lambda. Those two lambdas are duplicates, and have a rather large contextual concern. Below is a copy:

(lambda(completions) (
    xah-filter-list
    (lambda(item) (string-match-p (concat "\\W" ,project "\\W") (first item)))
    completions)
))

Let’s step through this from the inside out.

The following code (lambda(item) (string-match-p (concat "\\W" ,project "\\W") (first item)) creates is the filter. The given item is list from a completion result alist. The first item of the list is a string. The lambda returns true if there’s a match. Let’s replace this logic with the symbol predicate-matching-function.

Stepping out we have (xah-filter-list predicate-matching-function completions). In this section I’m calling xah-filter-list with the function predicate-matching-function and completions, an alist of candidate results. xah-filter-list returns a new alist in which each completion is a match. We use the predicate-matching-function to determine if its a match. Let’s replace this with completions-filtering-function.

Stepping out again, we now have lambda(completions) (completions-filtering-function); This is what we will use as the filter-fn parameter for both org-roam-find-file and org-roam-insert. Both methods require that filter-fn be a function that takes one parameter; eg. the alist of completions.

I could refactor this, but at this stage it’s good enough for my needs.

And if I start refactoring, I’ll need to look at consolidating the two macros that each generate a function. And I might look at my menu generation.

Wrapping Things Up

I love the malleability of Emacs. I love that its a text editor; What I really love is that I can build on the vast array of functions to help me organize according to my needs.

-1:-- Revisiting Hydra Menu for Org Roam Lookup in Emacs (Post Jeremy Friesen (jeremy@takeonrules.com))--L0--C0--December 09, 2020 01:01 AM