Categories: geek » emacs » org

View topic page - RSS - Atom - Subscribe via email

Thinking about time travel with the Emacs text editor, Org Mode, and backups

| emacs, org

Sometimes I just need to rewind 15 minutes. That's the length of A+'s recess at virtual school, which she does at home. At recess, she often likes to get hugs and sometimes a snack. If I'm working on something that requires sustained thought, like code or a blog post, I can't hold those thoughts in my head for that long while cheerfully listening to A+ share the trivia she's read on the Stardew Valley wiki. If I try to keep my train of thought, I get grumpy. I'd rather get better at time travel instead. Naturally, this calls for Emacs.

For people who are unfamiliar with Emacs or Org Mode

GNU Emacs is a highly customizable program for editing text, writing code, and doing mostly whatever people want to get it to do. Org Mode is a package (or ecosystem of packages, really) that modifies GNU Emacs to make it easier to take notes, plan tasks, export documents, and so on. If you're not into Emacs yet, this post might be a little technical, but maybe there are ways to translate some of the ideas to things you're using.

What was I doing again?

Sometimes recess totally resets my brain and I can't even think of what I was just working on. To make it easier to hit the ground running, I try to make a habit of creating a task in Org Mode before I start working on it. Or, more realistically, halfway through, when I realize I have to first do another thing, so then I jot down a quick task for the work I was previously doing and another task for the tangent I'm about to go on. That way, I can quickly check my notes to see what I was doing. org-capture (which I've bound to C-c r) is handy for that. I have a template (t) that creates a timestamped TODO that links to the context I created it in (files, note headings, etc.) and saves it to my inbox file. Then I can jump to my inbox file with a keyboard shortcut and look at what I need to get back to doing.

Sometimes I vaguely remember that I've already created a task for this before and I can find it with C-u C-c C-w (org-refile). When org-refile is called with a universal prefix argument (C-u), it will prompt for a heading in org-refile-targets and jump to it. I have it set to complete the outline path, so I can try to find things by project. Failing that, I might have a quick rummage in my inbox. I usually don't remember the exact words I used in the the task title, though. Maybe someday I'll get the hang of org-ql or p-search (Emacsconf talk on p-search), resurrect the Remembrance Agent so that it can continuously do bag-of-words matching, or use embeddings to find semantically similar tasks and notes. In the meantime, capturing the task is more important than avoiding duplicates. I can find and clean up duplicates later on.

All of that is moot when I'm away from my computer, which is most of the time. My phone is pretty handy for quick notes, though. I use Orgzly Revived to capture a quick note in my inbox. This gets synchronized with my Org Mode notes using Syncthing.

Hmm, I gotta do this first…

Often the interruption doesn't even come from outside, but from my brain's endless stream of interesting ideas. Some of those ideas can be saved as tasks to work on eventually, but sometimes I need to pause my current task and work on the new idea. I have a template for an interrupting task (i) that automatically clocks out of the previous task and clocks into the new one.

My template for interrupting tasks

This is the entry in my org-capture-templates.

        ("i" "Interrupting task" entry
         (file ,my-org-inbox-file)
         "* STARTED %^{Task}\n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n"
         :clock-in :clock-resume
         :prepend t)

Okay, that's done, what was I doing before?

If I clock into tasks, I can use org-clock-goto along with the C-u universal prefix (C-u C-c C-x C-j) to see a list of recently-clocked-in tasks. This is great for "popping the stack," which is how I think of backtracking once I finished an interrupting task.

I usually forget to clock out. That's okay. I'm not looking for precise total times, just breadcrumbs.

… What was I thinking?

Sometimes a few keywords aren't enough to jog my memory. Whenever I think, "Ah, this is easy, I don't need to take notes," I inevitably regret it. Sometimes I realize I have to re-do my thinking fifteen minutes later, when singing 4-Town songs with A+ has pushed those thoughts out of my brain. Sometimes I have to re-do my thinking several months later, which is even harder.

Notes are super-helpful. I love the way Org Mode lets me write notes, paste in hyperlinks, add snippets of code, save the results of my explorations, include my sketches, and even export them as blog posts or documents to share.

Sometimes I have to go back farther into the past

It can take me months or even years before I can circle back to a project or idea. It can be hard to reconstruct my thinking after a lot of time has passed, so it's good to write down as much as possible. Taking notes feels slower than just plunging ahead, but they help me travel back in time to try to remember.

This really gets hammered in when I run into things I've forgotten, like when I dusted off my time-tracking code so I could make some changes. In the four years that elapsed between Aug 2020 (my last change) and Oct 2024 (when I decided to upgrade it to the latest version of Rails), I'd forgotten how to even run a development version of my code. Whoops. I ended up taking more notes along the way.

I try to keep project-related notes as close to the project files as possible, like a README.org in the project directory. Sometimes I don't even remember what the project is called. I try to keep another file that indexes things on my computer as well as things in real life.

Sometimes I know I wrote tasks or notes down before but I can't remember the exact words I used for them. I'm curious about whether embeddings might help me find those things again. So far it's been okay to just add a new task or note, and then periodically clean up entries that are no longer needed.

Going sideways

Sometimes I want to visit alternate timelines, trying different ways to do something. I really like the way undo works in Emacs. It's different from most programs. Emacs keeps the things you undo/redo.

Let's say I start writing a paragraph or a piece of code. I change my mind about something. Maybe I undo, maybe I cut, maybe I delete. I write again. I change my mind again. The first way was better, maybe. I can go back to that, step through any of the intermediate changes, consider the other version again. It's not lost.

Actually navigating the Emacs undo history can be tricky. I like using the vundo package for that. It shows a compact view of the different branches of this timeline so that I can easily jump between them or compare them.

[2025-06-26 Thu] Check out Undo finally clicked with vundo | shom.dev for a screenshot and some explanation of vundo.

If I'm working on something more complicated, like code, I might make changes over several sessions. This is where version control is handy. I like using the Git version control system, especially with the Magit package. I can commit versions of the files manually along with a quick note about what I changed or what I'm trying. This allows me to easily reset to a certain point.

Sometimes I'm good about picking the right point to commit: I've made decent progress and things are working. Sometimes I realize only later on that I probably should have saved a commit a little while ago, and now I'm halfway through another idea that I'm not going to have time to finish and that leaves my project in a non-working state. In that situation, sometimes I'll use the visual undo provided by the vundo package to go backwards to a version that looks about right, save that file, commit it with a quick note, and then go forward in time again.

Saving revisions in Git makes it much easier to go backwards in time even if I've restarted my computer. magit-blame and vc-annotate give me slightly different views showing me the changes in a file. They don't show me information on deleted sections, though. For that, I can use the magit-diff command to compare versions. Sometimes it's easier to flip through the history of a single file with git-timemachine.

Git lets me name different experimental timelines (branches) and logically group changes together. It means I don't have to worry so much about messing up a working file, since I can travel back in time to that version. It also means I can easily compare them to see what I've changed so far.

In addition to using version control for various projects, I also save backup files to a separate directory by setting my backup-directory-alist to (("." . "~/.config/emacs/backups")). Disk space is cheap; might as well keep all the backups. I sometimes manually go into this directory to find older versions of things. It occurs to me that it might be good to flip through the backups in the same way that git-time-machine makes it easy to flip through git revisions. I'm trying out lewang/backup-walker, which shows the incremental diffs between versions. It was last updated 12 years ago(!), but can easily be dusted off to work with Emacs 30 by defining some functions that it's looking for. Here's my config snippet:

(use-package backup-walker
  :vc (:url "https://github.com/lewang/backup-walker")
  :init
  (defalias 'string-to-int 'string-to-number)
  (defalias 'display-buffer-other-window 'display-buffer))

Into the future

It's not all about going back to the past. Sometimes I want to plan ahead: tasks that I want to schedule for a certain date, pre-mortems to help me make decisions, gifts for my future self. I use Google Calendar for appointments and other things I might want to share with W- for planning, but there are lots of other things that aren't tied to a specific time and date. The agenda feature of Org Mode is handy for scheduling things and moving them around.

Scheduled tasks don't work out so well if my agenda gets cluttered by things I ignore, so if I find myself procrastinating something a lot, I think about whether I really want to do whatever it is I've written down.

Some notes aren't associated with specific dates, but with other events that might happen. I have an Org Mode outline with various subheadings under "In case of…", although I often forget to check these or have a hard time finding them again. Maybe someday I can write a script that analyzes the words I use in my journal entries or tasks and finds the notes that approximately match those keywords.

Things I want to try

Thinking out loud more might be worth experimenting with, since I can do that while I'm working in a different file. I've used my audio recorder to record braindumps and I have a workflow for transcribing those with OpenAI Whisper. I think it would be even more useful to have an org-capture equivalent so that I can capture the thought by audio, save the recording in case there are recognition errors (highly likely because of the technical terms), and save the context. Or maybe an even neater interface that keeps an ear out for keywords, executes commands based on them, and saves the rest as notes? whisper-ctranslate2 has a live_transcribe option that works reasonably well after a short delay, and maybe I can use a process filter to pull the information out or write a custom Python script.

I appreciate how working with plain text can help me jump backward or forward in time. I'm looking forward to seeing how this can be even better!

This post was inspired by Emacs Carnival 2025-06: Take Two • Christian Tietze and IndieWeb Carnival: Take Two. Check those out for related blog posts!

This is, in fact, my second take on the topic. =) Here's my first one: Making and re-making: fabric is tuition

View org source for this post

Run source blocks in an Org Mode subtree by custom ID

| emacs, org

I like the way Org Mode lets me logically group functions into headings. If I give the heading a CUSTOM_ID property (which is also handy for exporting to HTML, as it turns into an link anchor), I can use that property to find the subtree. Then I can use org-babel-execute-subtree to execute all source blocks in that subtree, which means I can mix scripting languages if I want to.

Here's the code:

(defun my-org-execute-subtree-by-custom-id (id &optional filename)
  "Prompt for a CUSTOM_ID value and execute the subtree with that ID.
If called with \\[universal-argument], prompt for a file, and then prompt for the ID."
  (interactive (if current-prefix-arg
                   (let ((file (read-file-name "Filename: ")))
                     (list
                      (with-current-buffer (find-file-noselect file)
                        (completing-read
                         "Custom ID: "
                         (org-property-values "CUSTOM_ID")))
                      file))
                 (list
                  (completing-read "Custom ID: " (org-property-values "CUSTOM_ID")))))
  (with-current-buffer (if filename (find-file-noselect filename) (current-buffer))
    (let ((pos (org-find-property "CUSTOM_ID" id)))
      (if pos
          (org-babel-execute-subtree)
        (if filename(error "Could not find %s in %s" id filename)
          (error "Could not find %s" id))))))

For example, in Using Org Mode, Emacs Lisp, and TRAMP to parse meetup calendar entries and generate a crontab, I have a Emacs Lisp source block that generates a crontab on a different computer, and a shell source block that installs it on that computer.

Technical notes: org-babel-execute-subtree narrows to the current subtree, so if I want anything from the rest of the buffer, I need to widen the focus again. Also, it's wrapped in a save-restriction and a save-excursion, so someday I might want to figure out how to handle the cases where I want to change what I'm looking at.

elisp: links in Org Mode let me call functions by clicking on them or following them with C-c C-o (org-open-at-point). This means I can make links that execute subtrees that might even be in a different file. For example, I can define links like these:

  • [[elisp:(my-org-execute-subtree-by-custom-id "update" "~/sync/emacs-calendar/README.org")][Update Emacs calendar]]
  • [[elisp:(my-org-execute-subtree-by-custom-id "crontab" "~/sync/emacs-calendar/README.org")][Update Emacs meetup crontab]]

That could be a good starting point for a dashboard.

Related: Execute a single named Org Babel source block

This is part of my Emacs configuration.
View org source for this post

Using Org Mode, Emacs Lisp, and TRAMP to parse meetup calendar entries and generate a crontab

| org, emacs

Times and time zones trip me up. Even with calendar notifications, I still fumble scheduled events. Automation helps me avoid embarrassing hiccups.

We run BigBlueButton as a self-hosted web conferencing server for EmacsConf. It needs at least 8 GB of RAM when active. When it's dormant, it fits on a 1 GB RAM virtual private server. It's easy enough to scale the server up and down as needed. Using the server for Emacs meetups in between EmacsConfs gives people a way to get together, and it also means I can regularly test the infrastructure. That makes scaling it up for EmacsConf less nerve-wracking.

I have some code that processes various Emacs meetup iCalendar files (often with repeating entries) and combines them into one iCal file that people can subscribe to calendar, as well as Org files in different timezones that they can include in their org-agenda-files. The code I use to parse the iCal seems to handle time zones and daylight savings time just fine. I set it up so that the Org files have simple non-repeating entries, which makes them easy to parse. I can use the Org file to determine the scheduled jobs to run with cron on a home server (named xu4) that's up all the time.

This code parses the Org file for schedule information, then generates pairs of crontab entries. The first entry scales the BigBlueButton server up 1 hour before the event using my bbb-testing script, and the second entry scales the server down 6 hours after the event using my bbb-dormant script (more info). That gives organizers time to test it before the event starts, and it gives people plenty of time to chat. A shared CPU 8 GB RAM Linode costs USD 0.072 per hour, so that's USD 0.50 per meetup hosted.

Using #+begin_src emacs-lisp :file "/ssh:xu4:~/bbb.crontab" :results file as the header for my code block and using an SSH agent for authentication lets me use TRAMP to write the file directly to the server. (See Results of Evaluation (The Org Manual))

(let* ((file "/home/sacha/sync/emacs-calendar/emacs-calendar-toronto.org")
       (time-format "%M %H %d %m")
       (bbb-meetups "OrgMeetup\\|Emacs Berlin\\|Emacs APAC")
       (scale-up "/home/sacha/bin/bbb-testing")
       (scale-down "/home/sacha/bin/bbb-dormant"))
  (mapconcat
   (lambda (o)
     (let ((start-time (format-time-string time-format (- (car o) 3600 )))
           (end-time (format-time-string time-format (+ (car o) (* 6 3600)))))
       (format "# %s\n%s * %s\n%s * %s\n"
               (cdr o)
               start-time
               scale-up
               end-time
               scale-down)))
   (delq nil
         (with-temp-buffer
           (insert-file-contents file)
           (org-mode)
           (goto-char (point-min))
           (org-map-entries
            (lambda ()
              (when (and
                     (string-match bbb-meetups (org-entry-get (point) "ITEM"))
                     (re-search-forward org-tr-regexp (save-excursion (org-end-of-subtree)) t))
                (let ((time (match-string 0)))
                  (cons (org-time-string-to-seconds time)
                        (format "%s - %s" (org-entry-get (point) "ITEM") time)))))
            "LEVEL=1")))
   "\n"))

The code makes entries that look like this:

# OrgMeetup (virtual) - <2025-06-11 Wed 12:00>--<2025-06-11 Wed 14:00>
00 11 11 06 * /home/sacha/bin/bbb-testing
00 18 11 06 * /home/sacha/bin/bbb-dormant

# Emacs Berlin (hybrid, in English) - <2025-06-25 Wed 12:30>--<2025-06-25 Wed 14:30>
30 11 25 06 * /home/sacha/bin/bbb-testing
30 18 25 06 * /home/sacha/bin/bbb-dormant

# Emacs APAC: Emacs APAC meetup (virtual) - <2025-06-28 Sat 04:30>--<2025-06-28 Sat 06:00>
30 03 28 06 * /home/sacha/bin/bbb-testing
30 10 28 06 * /home/sacha/bin/bbb-dormant

This works because meetups don't currently overlap. If there were, I'll need to tweak the code so that the server isn't downscaled in the middle of a meetup. It'll be a good problem to have.

I need to load the crontab entries by using crontab bbb.crontab. Again, I can tell Org Mode to run this on the xu4 home server. This time I use the :dir argument to specify the default directory, like this:

#+begin_src sh :dir "/ssh:xu4:~" :results silent
crontab bbb.crontab
#+end_src

Then cron can take care of things automatically, and I'll just get the e-mail notifications from Linode telling me that the server has been resized. This has already come in handy, like when I thought of Emacs APAC as being on Saturday, but it was actually on Friday my time.

I have another Emacs Lisp block that I use to retrieve all the info and update the list of meetups. I can add (goto-char (org-find-property "CUSTOM_ID" "crontab")) to find this section and use org-babel-execute-subtree to execute all the code blocks. That makes it an automatic part of my process for updating the Emacs Calendar and Emacs News. Here's the code that does the calendar part (Org source):

(defun my-prepare-calendar-for-export ()
  (interactive)
  (with-current-buffer (find-file-noselect "~/sync/emacs-calendar/README.org")
    (save-restriction
      (widen)
      (goto-char (point-min))
      (re-search-forward "#\\+NAME: event-summary")
      (org-ctrl-c-ctrl-c)
      (org-export-to-file 'html "README.html")
      ;; (unless my-laptop-p (my-schedule-announcements-for-upcoming-emacs-meetups))
      ;; update the crontab
      (goto-char (org-find-property "CUSTOM_ID" "crontab"))
      (org-babel-execute-subtree)
      (when my-laptop-p
        (org-babel-goto-named-result "event-summary")
        (re-search-forward "^- ")
        (goto-char (match-beginning 0))
        (let ((events (org-babel-read-result)))
          (oddmuse-edit "EmacsWiki" "Usergroups")
          (goto-char (point-min))
          (delete-region (progn (re-search-forward "== Upcoming events ==\n\n") (match-end 0))
                         (progn (re-search-forward "^$") (match-beginning 0)))
          (save-excursion (insert (mapconcat (lambda (s) (concat "* " s "\n")) events ""))))))))
(my-prepare-calendar-for-export)

I used a similar technique to generate the EmacsConf crontabs for automatically switching to the next talk. For that one, I used Emacs Lisp to write the files directly instead of using the :file header argument for Org Mode source blocks. That made it easier to loop over multiple files.

Hmm. Come to think of it, the technique of "go to a specific subtree and then execute it" is pretty powerful. In the past, I've found it handy to execute source blocks by name. Executing a subtree by custom ID is even more useful because I can easily mix source blocks in different languages or include other information. I think that's worth adding a my-org-execute-subtree-by-custom-id function to my Emacs configuration. Combined with an elisp: link, I can make links that execute functional blocks that might even be in different files. That could be a good starting point for a dashboard.

I love the way Emacs can easily work with files and scripts in different languages on different computers, and how it can help me with times and time zones too. This code should help me avoid brain hiccups and calendar mixups so that people can just enjoy getting together. Now I don't have to worry about whether I remembered to set up cron entries and if I did the math right for the times. We'll see how it holds up!

View org source for this post

Org Mode: Cutting the current list item (including nested lists) with a speed command

| emacs, org

Defining shortcuts in org-speed-commands is handy because you can use these single-key shortcuts at the beginning of a subtree. With a little modification, they'll also work at the beginning of list items.

(defun my-org-use-speed-commands-for-headings-and-lists ()
  "Activate speed commands on list items too."
  (or (and (looking-at org-outline-regexp) (looking-back "^\**" nil))
      (save-excursion (and (looking-at (org-item-re)) (looking-back "^[ \t]*" nil)))))
(setq org-use-speed-commands 'my-org-use-speed-commands-for-headings-and-lists)

I want k to be an org-speed-commands that cuts the current subtree or list item. This is handy when I'm cleaning up the Mastodon toots in my weekly review or getting rid of outline items that I no longer need. By default, k is mapped to org-cut-subtree, but it's easy to override.

(defun my-org-cut-subtree-or-list-item (&optional n)
  "Cut current subtree or list item."
  (cond
   ((and (looking-at org-outline-regexp) (looking-back "^\**" nil))
    (org-cut-subtree n))
   ((looking-at (org-item-re))
    (kill-region (org-beginning-of-item) (org-end-of-item)))))
(with-eval-after-load 'org
  (setf (alist-get "k" org-speed-commands nil nil #'string=)
        #'my-org-cut-subtree-or-list-item))

So now, if I put my cursor before "1." below and press k:

- this
  1. is a
    - nested
  2. list
- with levels

it will turn into:

  • this
    1. list
  • with levels

You can find out a little more about Org Mode speed commands in the Org manual: (info "(org) Speed Keys").

This is part of my Emacs configuration.
View org source for this post

Tweaking my 11ty blog to link to the Mastodon post defined in an Org Mode property

| 11ty, mastodon, org

One of the things I like about blogging from Org Mode in Emacs is that it's easy to add properties to the section that I'm working on and then use those property values elsewhere. For example, I've modified Emacs to simplify tooting a link to my blog post and saving the Mastodon status URL in the EXPORT_MASTODON property. Then I can use that in my 11ty static site generation process to include a link to the Mastodon thread as a comment option.

First, I need to export the property and include it in the front matter. I use .11tydata.json files to store the details for each blog post. I modified ox-11ty.el so that I could specify functions to change the front matter (org-11ty-front-matter-functions, org-11ty--front-matter):

(defvar org-11ty-front-matter-functions nil
  "Functions to call with the current front matter plist and info.")
(defun org-11ty--front-matter (info)
  "Return front matter for INFO."
  (let* ((date (plist-get info :date))
         (title (plist-get info :title))
         (modified (plist-get info :modified))
         (permalink (plist-get info :permalink))
         (categories (plist-get info :categories))
         (collections (plist-get info :collections))
         (extra (if (plist-get info :extra) (json-parse-string
                                             (plist-get info :extra)
                                             :object-type 'plist))))
    (seq-reduce
     (lambda (prev val)
       (funcall val prev info))
     org-11ty-front-matter-functions
     (append
      extra
      (list :permalink permalink
            :date (if (listp date) (car date) date)
            :modified (if (listp modified) (car modified) modified)
            :title (if (listp title) (car title) title)
            :categories (if (stringp categories) (split-string categories) categories)
            :tags (if (stringp collections) (split-string collections) collections))))))

Then I added the EXPORT_MASTODON Org property as part of the front matter. This took a little figuring out because I needed to pass it as one of org-export-backend-options, where the parameter is defined as MASTODON but the actual property needs to be called EXPORT_MASTODON.

(defun my-org-11ty-add-mastodon-to-front-matter (front-matter info)
  (plist-put front-matter :mastodon (plist-get info :mastodon)))
(with-eval-after-load 'ox-11ty
  (cl-pushnew
   '(:mastodon "MASTODON" nil nil)
   (org-export-backend-options (org-export-get-backend '11ty)))
  (add-hook 'org-11ty-front-matter-functions #'my-org-11ty-add-mastodon-to-front-matter))

Then I added the Mastodon field as an option to my comments.cjs shortcode. This was a little tricky because I'm not sure I'm passing the data correctly to the shortcode (sometimes it ends up as item.data, sometimes it's item.data.data, …?), but with ?., I can just throw all the possibilities in there and it'll eventually find the right one.

const pluginRss = require('@11ty/eleventy-plugin-rss');
module.exports = function(eleventyConfig) {
  function getCommentChoices(data, ref) {
    const mastodonUrl = data.mastodon || data.page?.mastodon || data.data?.mastodon;
    const mastodon = mastodonUrl && `<a href="${mastodonUrl}" target="_blank" rel="noopener noreferrer">comment on Mastodon</a>`;
    const url = ref.absoluteUrl(data.url || data.permalink || data.data?.url || data.data?.permalink, data.metadata?.url || data.data?.metadata?.url);
    const subject = encodeURIComponent('Comment on ' + url);
    const body = encodeURIComponent("Name you want to be credited by (if any): \nMessage: \nCan I share your comment so other people can learn from it? Yes/No\n");
    const email = `<a href="mailto:sacha@sachachua.com?subject=${subject}&body=${body}">e-mail me at sacha@sachachua.com</a>`;
    const disqusLink = url + '#comment';
    const disqusForm = data.metadata?.disqusShortname && `<div id="disqus_thread"></div>
<script>
 var disqus_config = function () {
   this.page.url = "${url}";
   this.page.identifier = "${data.id || ''} ${data.metadata?.url || ''}?p=${ data.id || data.permalink || this.page?.url}";
   this.page.disqusTitle = "${ data.title }"
   this.page.postId = "${ data.id || data.permalink || this.page?.url }"
 };
 (function() { // DON'T EDIT BELOW THIS LINE
   var d = document, s = d.createElement('script');
   s.src = 'https://${ data.metadata?.disqusShortname }.disqus.com/embed.js';
   s.setAttribute('data-timestamp', +new Date());
   (d.head || d.body).appendChild(s);
 })();
</script>
<noscript>Disqus requires Javascript, but you can still e-mail me if you want!</noscript>`;
    return { mastodon, disqusLink, disqusForm, email };
  }
  eleventyConfig.addShortcode('comments', function(data, linksOnly=false) {
    const { mastodon, disqusForm, disqusLink, email } = getCommentChoices(data, this);
    if (linksOnly) {
      return `You can ${mastodon ? mastodon + ', ' : ''}<a href="${disqusLink}">comment with Disqus (JS required)</a>${mastodon ? ',' : ''} or ${email}.`;
    } else {
      return `<div id="comment"></div>
You can ${mastodon ? mastodon + ', ' : ''}comment with Disqus (JS required)${mastodon ? ', ' : ''} or you can ${email}.
${disqusForm || ''}`;}
  });
}

I included it in my post.cjs shortcode:

module.exports = eleventyConfig =>
eleventyConfig.addShortcode('post', async function(item, index, includeComments) {
  let comments = '<div class="comments">' + (includeComments ? this.comments(item) : this.comments(item, true)) + '</div>';
  let categoryList = item.categories || item.data && item.data.categories;
  let categoriesFooter = '', categories = '';
  if (categoryList && categoryList.length > 0) {
    categoriesFooter = `<div class="footer-categories">More posts about ${this.categoryList(categoryList)}</div>`;
    categories = `| <span class="categories">${this.categoryList(categoryList)}</span>`;
  }

  return  `<article class="post" id="index${index}" data-url="${item.url || item.permalink || ''}">
<header><h2 data-pagefind-meta="title"><a href="${item.url || item.permalink || ''}">${item.title || item.data && item.data.title}</a></h2>
${this.timeInfo(item)}${categories}
</header>
<div class="entry">
${await (item.templateContent || item.layoutContent || item.data?.content || item.content || item.inputContent)}
</div>
${comments}
${categoriesFooter}
</article>`;
});

I also included it in my RSS item template to make it easier for people to send me comments without having to dig through my website for contact info.

const posthtml = require("posthtml");
const urls = require("posthtml-urls");

module.exports = (eleventyConfig) => {
  eleventyConfig.addAsyncShortcode('rssItem', async function(item) {
    let content = item.templateContent.replace(/--/g, '&#45;&#45;');
    if (this.transformWithHtmlBase) {
      content = await this.transformWithHtmlBase(content);
    }
    return `<item>
    <title>${item.data.title}</title>
    <link>${this.absoluteUrl(item.url, item.data.metadata.url)}</link>
    <dc:creator><![CDATA[${item.data.metadata.author.name}]]></dc:creator>
    <pubDate>${item.date.toUTCString()}</pubDate>
    ${item.data.categories?.map((cat) => `<category>${cat}</category>`).join("\n") || ''}
    <guid isPermaLink="false">${this.guid(item)}</guid>
    <description><![CDATA[${content}
<p>${this.comments(item, true)}</p>]]></description>
    </item>`;
  });
};

The new workflow I'm trying out seems to be working:

  1. Keep npx eleventy --serve running in the background, using .eleventyignore to make rebuilds reasonably fast.
  2. Export the subtree with C-c e s 1 1, which uses org-export-dispatch to call my-org-11ty-export with the subtree.
  3. After about 10 seconds, use my-org-11ty-copy-just-this-post and verify.
  4. Use my-mastodon-11ty-toot-post to compose a toot. Edit the toot and post it.
  5. Check that the EXPORT_MASTODON property has been set.
  6. Export the subtree again, this time with the front matter.
  7. Publish my whole blog.

Next, I'm thinking of modifying my-mastodon-11ty-toot-post so that it includes a list of links to blog posts I might be building on or responding to, and possibly the handles of people related to those blog posts or topics. Hmm…

View org source for this post

mastodon.el: Copy toot content as Org Mode

| mastodon, org, emacs

Sometimes I want to copy a toot and include it in my Org Mode notes, like when I post a thought and then want to flesh it out into a blog post. This code defines my-mastodon-org-copy-toot-content, which converts the toot text to Org Mode format using Pandoc and puts it in the kill ring so I can yank it somewhere else.

(defun my-mastodon-toot-at-url (&optional url)
  "Return JSON toot object at URL.
If URL is nil, return JSON toot object at point."
  (if url
      (let* ((search (format "%s/api/v2/search" mastodon-instance-url))
             (params `(("q" . ,url)
                       ("resolve" . "t"))) ; webfinger
             (response (mastodon-http--get-json search params :silent)))
        (car (alist-get 'statuses response)))
    (mastodon-toot--base-toot-or-item-json)))

(defun my-mastodon-org-copy-toot-content (&optional url)
  "Copy the current toot's content as Org Mode.
Use pandoc to convert.

When called with \\[universal-argument], prompt for a URL."
  (interactive (list
                (when current-prefix-arg
                  (read-string "URL: "))))

  (let ((toot (my-mastodon-toot-at-url url)))
    (with-temp-buffer
      (insert (alist-get 'content toot))
      (call-process-region nil nil "pandoc" t t nil "-f" "html" "-t" "org")
      (kill-new
       (concat
        (org-link-make-string
         (alist-get 'url toot)
         (concat "@" (alist-get 'acct (alist-get 'account toot))))
        ":\n\n#+begin_quote\n"
        (string-trim (buffer-string)) "\n#+end_quote\n"))
      (message "Copied."))))
This is part of my Emacs configuration.
View org source for this post

Org Mode: Merge top-level items in an item list

| org

I usually summarize Mastodon links, move them to my Emacs News Org file, and then categorize them. Today I accidentically categorized the links while they were still in my Mastodon buffer, so I had two lists with categories. I wanted to write some Emacs Lisp to merge sublists based on the top-level items. I could sort the list alphabetically with C-c ^ (org-sort) and then delete the redundant top-level item lines, but it's fun to tinker with Emacs Lisp.

Example input:

  • Topic A:
    • Item 1
    • Item 2
      • Item 2.1
  • Topic B:
    • Item 3
  • Topic A:
    • Item 4
      • Item 4.1

Example output:

  • Topic B:
    • Item 3
  • Topic A:
    • Item 1
    • Item 2
      • Item 2.1
    • Item 4
      • Item 4.1

The sorting doesn't particularly matter to me, but I want the things under Topic A to be combined. Someday it might be nice to recursively merge other entries (ex: if there's another "Topic A: - Item 2" subitem like "Item 2.2"), but I don't need that yet.

Anyway, we can parse the list with org-list-to-lisp (which can even delete the original list) and recreate it with org-list-to-org, so then it's a matter of transforming the data structure.

(defun my-org-merge-list-entries-at-point ()
  "Merge entries in a nested Org Mode list at point that have the same top-level item text."
  (interactive)
  (save-excursion
    (let* ((list-indentation (save-excursion
                               (goto-char (caar (org-list-struct)))
                               (current-indentation)))
           (list-struct (org-list-to-lisp t))
           (merged-list (my-org-merge-list-entries list-struct)))
      (insert (org-ascii--indent-string (org-list-to-org merged-list) list-indentation)
              "\n"))))

(defun my-org-merge-list-entries (list-struct)
  "Merge an Org list based on its top-level headings"
  (cons (car list-struct)
        (mapcar
         (lambda (g)
           (list
            (car g)
            (let ((list-type (car (car (cdr (car (cdr g))))))
                  (entries (seq-mapcat #'cdar (mapcar #'cdr (cdr g)))))
              (apply #'append (list list-type) entries nil))))
         (seq-group-by #'car (cdr list-struct)))))

A couple of test cases:

(ert-deftest my-org-merge-list-entries ()
  (should
   (equal
    (my-org-merge-list-entries
     '(unordered ("Topic B:" (unordered ("Item 3")))))
    '(unordered ("Topic B:" (unordered ("Item 3"))))))
  (should
   (equal
    (my-org-merge-list-entries
     '(unordered ("Topic B:" (unordered ("Item 3")))
                 ("Topic A:"
                  (unordered ("Item 1")
                             ("Item 2"
                              (unordered ("Item 2.1")))))
                 ("Topic A:"
                  (unordered
                   ("Item 4" (unordered ("Item 4.1")))))))
    '(unordered
      ("Topic B:" (unordered ("Item 3")))
      ("Topic A:"
       (unordered ("Item 1")
                  ("Item 2" (unordered ("Item 2.1")))
                  ("Item 4" (unordered ("Item 4.1")))))))))
View org source for this post