Week ending March 28, 2025: mastodon.el tweaks, search, workflows

| review, weekly
  • I've been practising fretting less about homework.
  • I added an On this day page to my blog. (blog post about it)
  • I added Mastodon links to my blog. I think the process will be: post the blog post; toot to Mastodon; edit the blog post and republish. I might be able to save time and just copy over the blog post during the first go-around, from make serve.
  • I added Pagefind search to my blog.
  • I wrote about some of my workflows.
  • I started a /now page.
  • Oops: I forgot to check on Emacs Berlin and it turned out that the NAS timezone was set to GMT-5 instead of America/Toronto, so I scrambled to get it set up. I also got distracted while trying to figure out how to revoke the token the NAS was using so it wouldn't downscale automatically, so that might have wrapped up the meeting early. I set up cronjobs on xu4 for next time.

Next week:

  • Continue to reduce fretting about homework.
  • Work through intermediate piano course in Simply Piano. Practise1 more songs, too.
  • Take a look at that inbox and start dusting things off.

Blog posts

Sketches

Toots

  • eleventy-post-graph (toot) I used eleventy-post-graph to add a quick year visualization to my year pages (2025, 2024, …) and a visualization for the whole blog. Someday it might be nice to make it more accessible and figure out how I can link to the blog post(s) for that day.
  • From @johnrakestraw's On keeping a notebook (toot)

    “One thing that really fascinates me is how I'm reminded of events and readings that I'd completely forgotten – but, once reminded, I find that these things are once again in my mind. Perhaps I can say what I'm thinking more clearly — though I'm more than a little frustrated by having absolutely no memory of experiencing or reading something I describe in an entry written only a few years ago, I'm fascinated by how reading what I wrote has brought that experience back to mind rather vividly. Of course I'm reminded of what I described in the text that I'm now re-reading, but I can also remember other things associated with whatever it is that is described there. It's as though the small bit that I wrote and can now read is the key that unlocks a much larger trove of memory. Funny how the mind works.”

    I am also quite fuzzy about things that happened, and I'm glad I've got notes to help me sort of remember.

  • Added comment links to my RSS feed (toot) Nudged by A Walk Through My Digital Neighborhood // Take on Rules by @takeonrules and also my recent focus on having more conversations around blog post ideas (and sometimes the annoyance of finding someone's contact info), I added comment links to my RSS/Atom items (https://sachachua.com/blog/feed/index.xml and https://sachachua.com/blog/feed/atom/index.xml, and also all the categories have feeds generally at category/…/feed/index.xml). If I've set a Mastodon URL for the entry, it'll link to the Mastodon thread too. #11ty
  • Switching to Bigger Picture for the lightbox (toot) Lightbox: I replaced PhotoSwipe with Bigger Picture seems nice and flexible
  • Connections (toot) Following a link from https://manuelmoreale.com/pb-maya , I enjoyed this quote about blogging:

    Although, as well researched and as thoughtful as Houston might be there's a messiness at work here that I love; it is the true great quality of a blog. That permission to roam, to let your curiosity grab you by the lapel and hoist you across fifteen different subjects over the course of a single paragraph; blogging is pointing at things and falling in love.

  • Bull sharks and respiration (toot) My 2021 post on A list of sharks that are obligate ram ventilators continues to pop up every now and then. Someone had a question about whether bull sharks are obligate ram ventilators, so I did a little research and added whatever notes I could find there. I think maybe they aren't, although they're sometimes described as such? Not sure, maybe someone can chime in. =)
  • Programmable Notes (toot) Oooh, it could be fun to trawl through these for ideas for things to port over to Emacs.

    The Smartblocks plug-in for Roam Research is the system I personally use to build these types of workflows. It offers a set of triggers, variables, and commands you can chain together into fairly readable statements like: <%SET:topOfMindToday,<%INPUT:What's on your mind today?%>%> or <%RANDOMBLOCKFROM:Writing Ideas%>.

    Even with limited programming knowledge, many people in the community have been able to fashion their own Smartblock flows. Plenty of them have published their workflows to the community Github for others to use.

    Smartblock flows on Github

  • The promise and distraction of productivity and note-taking systems (toot)

    Books are maps to territories that are completely internal to the reader. By focusing so heavily on extracting the surface symbology of the map itself, these process-heavy note-takers risk losing sight of the territory. A book's territory is the reasoning and argument that the book presents to you as a path you take through your own psyche. The goal isn't to remember everything the book contains. Remembering a book's contents is useless. The book exists to contain what it contains. If the contents are important, you keep a copy of it for you to look things up again.

    But that isn't the point of reading. The purpose of reading is to be changed. Sometimes the change is trivial and temporary – a piece of fiction that brings some joy in your life. Sometimes the change is profound – a shift in your perspective on life. “Action items” from a book are external and forcing yourself to follow through on them is exhausting.

  • Added Pagefind search (toot) I'm also experimenting with using Pagefind to provide search for my static site using client-side Javascript. It currently analyzes 10934 files and indexes 8183 pages (87272 words) in 40 seconds. The data is 125MB, but a search for, say, "sketchnote" transfers only 280KB, so that's pretty good. I think I'm adding the date properly and I know I can set that as the default sort, but I haven't yet figured out how to make it possible for people to sort by either relevance or date as they want. I also want to eventually format the search results to include the date. Maybe Building a Pagefind UI – dee.underscore.world will be useful.
Time
Category The other week % Last week % Diff % h/wk Diff h/wk
Unpaid work 3.3 4.7 1.4 7.9 2.4
Discretionary - Productive 19.2 20.1 0.9 33.7 1.5
Personal 9.4 9.9 0.5 16.6 0.8
Discretionary - Play 1.2 1.6 0.4 2.7 0.7
Discretionary - Family 0.0 0.3 0.3 0.5 0.5
A- 31.6 31.5 -0.1 53.0 -0.1
Business 1.7 0.8 -0.9 1.3 -1.5
Sleep 33.7 31.1 -2.5 52.3 -4.3
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

Feline feelings

| drawing, cat

Feel free to use this under the Creative Commons Attribution License.

Text from sketch

Feline feelings sachachua.com/2025-03-26-01

  • happy
    • playful
    • content
    • interested
    • proud
    • accepted
    • powerful
    • peaceful
    • trusting
    • optimistic
  • surprised
    • startled
    • confused
    • amazed
    • excited
  • bad
    • tired
    • busy
    • stressed
    • bored
  • fearful
    • scared
    • anxious
    • weak
    • rejected
    • insecure
    • threatened
  • angry
    • let down
    • humiliated
    • bitter
    • mad
    • aggressive
    • frustrated
    • distant
    • critical
  • disgusted
    • disapproving
    • disappointed
    • awful
    • repelled
  • sad
    • lonely
    • vulnerable
    • despair
    • guilty
    • depressed
    • hurt

Feelings wheel by Geoffrey Roberts

I want to draw more expressively, and experimenting with distinguishing between emotions seems like a good start. I followed up on our idea of drawing cats after Stick figure out feelings. It was a lot of fun drawing various kitties based on Geoffrey Roberts' emotion wheel. It turns out I'm still sometimes iffy on what a cat looks like in different poses, but maybe enough of the cat-ness has come through in these little doodles. =)

Related posts:

You might also like:

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

mastodon.el: Collect handles in clipboard (Emacs kill ring)

| mastodon, emacs

I sometimes want to thank a bunch of people for contributing to a Mastodon conversation. The following code lets me collect handles in a single kill ring entry by calling it with my point over a handle or a toot, or with an active region.

(defvar my-mastodon-handle "@sacha@social.sachachua.com")
(defun my-mastodon-copy-handle (&optional start-new beg end)
  "Append Mastodon handles to the kill ring.

Use the handle at point or the author of the toot.  If called with a
region, collect all handles in the region.

Append to the current kill if it starts with @. If not, start a new
kill. Call with \\[universal-argument] to always start a new list.

Omit my own handle, as specified in `my-mastodon-handle'."
  (interactive (list current-prefix-arg
                     (when (region-active-p) (region-beginning))
                     (when (region-active-p) (region-end))))
  (let ((handle
         (if (and beg end)
             ;; collect handles in region
             (save-excursion
               (goto-char beg)
               (let (list)
                 ;; Collect all handles from the specified region
                 (while (< (point) end)
                   (let ((mastodon-handle (get-text-property (point) 'mastodon-handle))
                         (button (get-text-property (point) 'button)))
                     (cond
                      (mastodon-handle
                       (when (and (string-match "@" mastodon-handle)
                                  (or (null my-mastodon-handle)
                                      (not (string= my-mastodon-handle mastodon-handle))))
                         (cl-pushnew
                          (concat (if (string-match "^@" mastodon-handle) ""
                                    "@")
                                  mastodon-handle)
                          list
                          :test #'string=))
                       (goto-char (next-single-property-change (point) 'mastodon-handle nil end)))
                      ((and button (looking-at "@"))
                       (let ((text-start (point))
                             (text-end (or (next-single-property-change (point) 'button nil end) end)))
                         (dolist (h (split-string (buffer-substring-no-properties text-start text-end) ", \n\t"))
                           (unless (and my-mastodon-handle (string= my-mastodon-handle h))
                             (cl-pushnew h list :test #'string=)))
                         (goto-char text-end)))
                      (t
                       ;; collect authors of toots too
                       (when-let*
                           ((toot (mastodon-toot--base-toot-or-item-json))
                            (author (and toot
                                         (concat "@"
                                                 (alist-get
                                                  'acct
                                                  (alist-get 'account (mastodon-toot--base-toot-or-item-json)))))))
                         (unless (and my-mastodon-handle (string= my-mastodon-handle author))
                           (cl-pushnew
                            author
                            list
                            :test #'string=)))
                       (goto-char (next-property-change (point) nil end))))))
                 (setq handle (string-join (seq-uniq list #'string=) " "))))
           (concat "@"
                   (or
                    (get-text-property (point) 'mastodon-handle)
                    (alist-get
                     'acct
                     (alist-get 'account (mastodon-toot--base-toot-or-item-json))))))))
    (if (or start-new (null kill-ring) (not (string-match "^@" (car kill-ring))))
        (kill-new handle)
      (dolist (h (split-string handle " "))
        (unless (member h (split-string " " (car kill-ring)))
          (setf (car kill-ring) (concat (car kill-ring) " " h)))))
    (message "%s" (car kill-ring))))

Another perk of tooting from Emacs using mastodon.el. =)

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

2025-03-24 Emacs news

| emacs, emacs-news

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

View org source for this post