I’m back and forth on whether to stick to org-gtd or to start using Things again. I wanted to re-tag some actions on the fly this morning and it took some extra typing I didn’t want to do. So far, my org-gtd and Denote usage haven’t intersected, and there is no way I can think of that any of the existing mobile tools could do anything for me at all if they did. So as I think about my Emacs/Denote/org estate, and how to decompose it and remix different pieces, there’s a pretty clean perforated line between written notes and tasks, provided I can find a way to bridge the two.

One thing I used to do when I was a regular Bear user was create a Things project for all the people I had regular 1:1s with. I set up section headings for each person in the project under which I dropped actions I owed or had delegated. At the top of that project, I had a list of links to my 1:1 notes with each person to make it easier to get to past notes during a meeting without looking them up in Bear – just save the URL to their 1:1 file, click the link, Bear opens to where I need it. This felt like a clean way to let two tools do what they’re best at: Things does have some basic Markdown editing, but I don’t generally want to embed writing/thinking in actions.

Unlike Bear, Emacs has the disadvantage of not being everywhere I would use Things, but since I’m exporting my Denote stuff to the web, my notes do have a permanent URL, and it’s derived from the filename of a given note, so it’s easy to convert the file path to the URL.

This function copies that URL to the clipboard when I’m in a note. No error- or sanity-checking: The results will be nonsense if I invoke it outside my Denote hierarchy, but I bound it to my Denote menu in Doom Emacs so it’ll only come up when it’s contextually appropriate:

(defun mph/convert-to-skyhook-url ()
  "Converts the current buffer's path to a URL for the Skyhook notes."
  (interactive)
  (let* ((buffer-path (buffer-file-name))
         (notes-directory "/Users/mph/org/notes/")
         (relative-path (file-relative-name buffer-path notes-directory))
         (html-path (concat (file-name-sans-extension relative-path) ".html"))
         (url-prefix "http://skyhook:8888/")
         (url (concat url-prefix html-path)))
    (kill-new url)
    (message "Skyhook URL: %s" url)))

Then it’s just dropping it into a Things project page and wrapping it in Markdown link markup.

There’s a converse linking relationship between todos and notes, and I made something to address that, too:

;; updated to de-hard-code the # symbol -- any symbol is sanitized for use in a URL now
(after! org
  (org-link-set-parameters "things"
    :follow (lambda (label) (browse-url (concat "things:///search?query=" (url-hexify-string label))))
    :export (lambda (path desc backend)
              (cond
               ((eq 'html backend)
                (format "<a href=\"things:///search?query=%s\">Things: %s</a>"
                        (url-hexify-string path)
                        (org-html-encode-plain-text path)))))))

It’s a custom org external link for Things. Enter a link of this format:

[[things:#foo]]

… and it will link to a Things search for #foo.

The :export section insures that when I publish the site, the things:// URL scheme survives. When org-mode comes across protocols it doesn’t recognize it mangles them. This ensures that things: URLs show up in the rendered HTML.

So if I’m sitting down to a 1:1 with “Joe Grudd,” I’ve got a link to the web version of his metanote from Things on a computer or phone. If I’m looking at Joe Grudd’s metanote in Emacs or on the web, I’ve got a link that shows a search for anything tagged #joeg in Things.

Automating dblock-driven metanote updates

I have metanotes set up in my Denote hierarchy for frequent people and topic tags. The metanotes are semi-automated at this point using Denote’s dynamic blocks.

#+BEGIN: denote-links :regexp "_rfc"
#+END:

Just C-c C-c in that block and it expands to something like:

#+BEGIN: denote-links :regexp "_rfc"
- [[denote:20230613T083549][RFC - Crosstraining]]
- [[denote:20230613T083700][RFC - IT Portfolio]]
- [[denote:20230613T083726][RFC - Status and progress]]
#+END:

All those Denote links are translated by org-publish during conversion to HTML, so a link to a metanote on the web server gets me to the HTML version of all the linked notes.

If I’m on a laptop, getting to the metanotes is pretty easy: They’re all in a metanote at the top of my Denote hierarchy, so that’s easy enough to get to in “full computer” contexts.

The biggest hole in this workflow is that that dynamic blocks have to be updated. I semi-automated it yesterday with a save hook in my Denote directory that updates any dblocks in a file before it saves, but you have to be in a metanote for that to happen.

Not sure if there’s a more efficient way to do it, but this automates the update process:

(defun mph/update-meta-dblocks (directory)
  "Update dynamic blocks, save, and publish HTML for files with '_meta' in the name in the given DIRECTORY."
  (interactive "DSelect directory: ")
  (let ((files (directory-files directory nil "\\.org$")))
    (dolist (file files)
      (when (string-match-p "_meta" file)
        (let ((org-file (concat directory "/" file)))
          (with-current-buffer (find-file-noselect org-file)
            ;; Suspend hooks temporarily
            (run-hooks 'no-hooks)
            (org-update-all-dblocks)
            (save-buffer)
            ))))))

(The run-hooks 'no-hooks business is there to keep my post-save publishing hooks from running with each touch, which I think will make this safe to use as a pre-processing hook.) I think I’m going to just use it as I remember it for a few days until I can see how much time it chews up. Maybe it’s better run as a scheduled batch thing now and then.

Cleaning up org publishing exports

I also took the time to clean up HTML publishing last night. Emacs documentation ftw: I used the publishing options page to run down settings/variables and what they do. In particular:

org-html-divs

I use SimpleCSS to save a few steps. org-html-divs lets you set the preamble, content, and postamble element types and id’s, so:

(setq org-html-divs
    '((preamble "header" "preamble") ;; set the preamble div to a <header> element with an id of "preamble"
      (content "main" "content") ;; etc.
      (postamble "footer" "postamble")))
(setq org-export-with-author nil)

I kept the id’s so I could have a reference for debugging and keep sight of org’s nomenclature.

org-html-preamble

To get a SimpleCSS nav into place, markup has to go into the export’s preamble:

(setq org-html-preamble "<nav><ul>
                           <li><a href=\"/\" class=\"home\">Home</a>
                           <li><a href=\"/sitemap.html\">All Notes</a>
                           </ul>
                         </nav>
                         ")

org-html-head

To pull in SimpleCSS, FuseJS, and a local style sheet:

(setq
      org-html-head "<link rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\" />
                     <link rel=\"stylesheet\" href=\"/local.css\" />
                     <script src=\"https://cdn.jsdelivr.net/npm/[email protected]\"></script>")

espanso

I don’t know what on Earth happened to TextExpander but wow it isn’t good.

Looking around for a text expansion tool of some kind I came across espanso. It’s a snippet tool. It behaves about like yasnippet or TextMate snippets: Start typing a trigger phrase and it expands it for you.

Most of these things are wrapped in a GUI. espanso is configurable with a YAML file. It also has some cool stuff for handling regexps that allow you to use variables to your snippets. For instance:

- regex: ":tml\\((?P<tag>.*)\\)"
  replace: "[Items tagged `{{tag}}` in Things](things:///search?query=%23{{tag}})"

If you type :tml(foo) it’ll expand to a Markdown link using foo.

[Items tagged `foo` in Things](things:///search?query=%23foo)

(I’m not sure, btw, how to do that in yasnippet and need to figure that out.)