<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/atom.xsl" type="text/xsl"?><feed
	xmlns="http://www.w3.org/2005/Atom"
	xmlns:thr="http://purl.org/syndication/thread/1.0"
	xml:lang="en-US"
	><title>Sacha Chua - category - org</title>
	<subtitle>Emacs, sketches, and life</subtitle>
	<link rel="self" type="application/atom+xml" href="https://sachachua.com/blog/category/org/feed/atom/index.xml" />
  <link rel="alternate" type="text/html" href="https://sachachua.com/blog/category/org" />
  <id>https://sachachua.com/blog/category/org/feed/atom/index.xml</id>
  <generator uri="https://11ty.dev">11ty</generator>
	<updated>2026-04-17T13:26:41Z</updated>
<entry>
		<title type="html">Create a Google Calendar event from an Org Mode timestamp</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2026/04/create-a-google-calendar-event-from-an-org-mode-timestamp/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2026-04-17T13:26:41Z</updated>
    <published>2026-04-17T13:26:41Z</published>
    <category term="org" />
<category term="emacs" />
		<id>https://sachachua.com/blog/2026/04/create-a-google-calendar-event-from-an-org-mode-timestamp/</id>
		<content type="html"><![CDATA[<p>
Time zones are hard, so I let calendaring systems take care of the conversion and confirmation. I've been using Google Calendar because it synchronizes with my phone and people know what to do with the event invite.
<a href="https://orgmode.org/manual/iCalendar-Export.html">Org Mode has iCalendar export</a>, but I sometimes have a hard time getting .ics files into Google Calendar on my laptop, so I might as well just create the calendar entry in Google Calendar directly. Well. Emacs is a lot more fun than Google Calendar, so I'd rather create the calendar entry from Emacs and put it into Google Calendar.
</p>

<p>
This function lets me start from a timestamp like <code>[2026-04-24 Fri 10:30]</code> (inserted with <code>C-u C-c C-!</code>, or <code>org-timestamp-inactive</code>) and create an event based on a template.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-time-zone</span> <span class="org-string">"America/Toronto"</span> <span class="org-doc">"Full name of time zone."</span>)

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-emacs-chat-schedule</span> (<span class="org-type">&amp;optional</span> time)
  <span class="org-doc">"Create a Google Calendar invite based on TIME or the Org timestamp at point."</span>
  (<span class="org-keyword">interactive</span> (list (sacha-org-time-at-point)))
  (browse-url
   (format
    <span class="org-string">"https://calendar.google.com/calendar/render?action=TEMPLATE&amp;text=%s&amp;details=%s&amp;dates=%s&amp;ctz=%s"</span>
    (url-hexify-string sacha-emacs-chat-title)
    (url-hexify-string sacha-emacs-chat-description)
    (format-time-string
     <span class="org-string">"%Y%m%dT%H%M%S"</span> time)
    sacha-time-zone)))

(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-emacs-chat-title</span> <span class="org-string">"Emacs Chat"</span> <span class="org-doc">"Title of calendar entry."</span>)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-emacs-chat-description</span>
  <span class="org-string">"All right, let's try this! =) See the calendar invite for the Google Meet link.</span>

<span class="org-string">Objective: Share cool stuff about Emacs workflows that's not obvious from reading configs, and have fun chatting about Emacs</span>

<span class="org-string">Some ideas for things to talk about:</span>
<span class="org-string">- Which keyboard shortcuts or combinations of functions work really well for you?</span>
<span class="org-string">- What's something you love about your setup?</span>
<span class="org-string">- What are you looking forward to tweaking next?</span>

<span class="org-string">Let me know if you want to do it on stream (more people can ask questions) or off stream (we can clean up the video in case there are hiccups). Also, please feel free to send me links to things you'd like me to read ahead of time, like your config!"</span>
  <span class="org-doc">"Description."</span>)
</code></pre>
</div>


<p>
It uses this function to convert the timestamp at point:
</p>

<p>
</p><details open=""><summary>sacha-org-time-at-point: Return Emacs time object for timestamp at point.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-org-time-at-point</span> ()
  <span class="org-doc">"Return Emacs time object for timestamp at point."</span>
  (org-timestamp-to-time (org-timestamp-from-string (org-element-property <span class="org-builtin">:raw-value</span> (org-element-context)))))

</pre></div></details>
<p></p>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#streaming">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2026/04/create-a-google-calendar-event-from-an-org-mode-timestamp/index.org">View Org source for this post</a></div><p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F04%2Fcreate-a-google-calendar-event-from-an-org-mode-timestamp%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Make chapter markers and video time hyperlinks easier to note while I livestream</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2026/04/make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2026-04-17T04:27:43Z</updated>
    <published>2026-04-17T04:27:43Z</published>
    <category term="org" />
<category term="emacs" />
		<id>https://sachachua.com/blog/2026/04/make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream/</id>
		<content type="html"><![CDATA[<p>
I want to make it easier to add chapter markers to my YouTube video descriptions and hyperlinks to specific times in videos in my blog posts.
</p>
<div id="outline-container-streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream-capture-timestamps" class="outline-3">
<h3 id="streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream-capture-timestamps"><a href="https://sachachua.com/blog/feed/atom/index.xml#streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream-capture-timestamps">Capture timestamps</a></h3>
<div class="outline-text-3" id="text-streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream-capture-timestamps">
<p>
Using wall-clock time via Org Mode timestamps like <span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T10:40:00-0400">[2026-04-16 Thu 10:40]</time></span> makes more sense to me than using video offsets because they're independent of any editing I might do.
</p>

<p>
<code>C-u C-c C-!</code> (<code>org-timestamp-inactive</code>) creates a timestamp with a time. I probably do often enough that I should create a Yasnippet for it:
</p>


<div class="org-src-container">
<pre class="src src-yasnippet"><code># -*- mode: snippet -*-
# name: insert time
# key: zt
# &#45;&#45;
`(format-time-string "[%Y-%m-%d %a %H:%M]")`
</code></pre>
</div>


<p>
I also have <a href="https://orgmode.org/manual/Capture.html">Org capture</a> templates, like this:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-capture</span>
  (add-to-list
   <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-capture-templates</span>
   <span class="org-highlight-quoted-quote">`</span>(<span class="org-string">"l"</span> <span class="org-string">"Timestamp"</span> item
     (file+headline ,sacha-stream-inbox-file <span class="org-string">"Timestamps"</span>)
     <span class="org-string">"- %U %i%?"</span>)))
</code></pre>
</div>


<p>
I've been experimenting with a <a href="https://sachachua.com/dotemacs#streaming-display-large-text-and-maybe-qr-code">custom Org Mode link type "stream:"</a> which:
</p>

<ul class="org-ul">
<li>displays the text in a larger font with a QR code for easier copying</li>
<li>sends the text to the YouTube chat via <a href="https://socialstream.ninja">socialstream.ninja</a></li>
<li>adds a timestamped note using the org-capture template above</li>
</ul>

<p>
Here is an example of that link in action. It's the <code>(Log)</code> link that I clicked on.
</p>

<details><summary>Let's extract that clip</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(compile-media-sync
 <span class="org-highlight-quoted-quote">'</span>((combined (<span class="org-builtin">:source</span>
              <span class="org-string">"/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs.mp4"</span>
              <span class="org-builtin">:original-start-ms</span> <span class="org-string">"51:09"</span>
              <span class="org-builtin">:original-stop-ms</span> <span class="org-string">"51:16"</span>))
   (combined (<span class="org-builtin">:source</span>
              <span class="org-string">"/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs-link-overlay.png"</span>
              <span class="org-builtin">:output-start-ms</span> <span class="org-string">"0:03"</span>
              <span class="org-builtin">:output-stop-ms</span> <span class="org-string">"0:04"</span>))
   (combined (<span class="org-builtin">:source</span>
              <span class="org-string">"/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs-qr-chat-overlay.png"</span>
              <span class="org-builtin">:output-start-ms</span> <span class="org-string">"0:05"</span>
              <span class="org-builtin">:output-stop-ms</span> <span class="org-string">"0:06"</span>)))
 <span class="org-string">"/home/sacha/proj/yay-emacs/ye16.1-stream-show-string-and-calculate-offset.mp4"</span>)
</code></pre>
</div>

</details>

<div class="media-post" id="orgc803fb4">
<p>
<video controls="1" src="https://sachachua.com/blog/2026/04/make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream/ye16.1-stream-show-string-and-calculate-offset.mp4" type="video/mp4"> <a href="https://sachachua.com/blog/2026/04/make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream/ye16.1-stream-show-string-and-calculate-offset.mp4">Download the video</a></video>
</p>

</div>

<p>
I used it in <a href="https://sachachua.com/blog/2026/04/ye16-sacha-and-prot-talk-emacs/">YE16: Sacha and Prot talk Emacs</a>. It was handy to have a link that I could click on instead of trying to remember a keyboard shortcut and type text.
For example, these are the timestamps that were filed under org-capture:
</p>

<ul class="org-ul">
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T10:39:00-0400">[2026-04-16 Thu 10:39]</time></span> Getting more out of livestreams</li>
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T10:57:00-0400">[2026-04-16 Thu 10:57]</time></span> Announcing livestreams</li>
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T11:05:00-0400">[2026-04-16 Thu 11:05]</time></span> Processing the recordings</li>
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T11:11:00-0400">[2026-04-16 Thu 11:11]</time></span> Non-packaged code</li>
</ul>

<p>
Here's a short function for getting those times:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp" id="org361bf8b"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-org-time-at-point</span> ()
  <span class="org-doc">"Return Emacs time object for timestamp at point."</span>
  (org-timestamp-to-time (org-timestamp-from-string (org-element-property <span class="org-builtin">:raw-value</span> (org-element-context)))))
</code></pre>
</div>


<p>
Next, I wanted to turn those timestamps into a hh:mm:ss offset into the streamed video.
</p>
</div>
</div>
<div id="outline-container-streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream-calculate-an-org-timestamp-s-offset-into-a-youtube-stream" class="outline-3">
<h3 id="streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream-calculate-an-org-timestamp-s-offset-into-a-youtube-stream"><a href="https://sachachua.com/blog/feed/atom/index.xml#streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream-calculate-an-org-timestamp-s-offset-into-a-youtube-stream">Calculate an Org timestamp's offset into a YouTube stream</a></h3>
<div class="outline-text-3" id="text-streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream-calculate-an-org-timestamp-s-offset-into-a-youtube-stream">
<p>
I post my YouTube videos under a brand account so that just in case I lose access to my main sacha@sachachua.com Google account, I still have access via my @gmail.com account. To enable YouTube API access to my channel, I needed to get my brand account's email address and set it up as a test user.
</p>

<ol class="org-ol">
<li>Go to <a href="https://myaccount.google.com/brandaccounts">https://myaccount.google.com/brandaccounts</a>.</li>
<li>Select the account.</li>
<li>Click on <b>View general account info</b></li>
<li>Copy the <code>...@pages.plusgoogle.com</code> email address there.</li>
<li>Go to <a href="https://console.cloud.google.com/">https://console.cloud.google.com/</a></li>
<li>Enable the YouTube data API for my project.</li>
<li>Download the credentials.json.</li>
<li>Go to <b>Data Access - Audience</b></li>
<li>Set the <b>User type</b> to <b>External</b></li>
<li>Add my brand account as one of the <b>Test users</b>.</li>
<li><p>
Log in at the command line:
</p>

<div class="org-src-container">
<pre class="src src-sh"><code> gcloud auth application-default login <span class="org-sh-escaped-newline">\</span>
     &#45;&#45;client-id-file=credentials.json <span class="org-sh-escaped-newline">\</span>
     &#45;&#45;scopes=<span class="org-string">"https://www.googleapis.com/auth/youtube"</span>
</code></pre>
</div>
</li>
</ol>

<p>
Then the following code calculates the offset of the timestamp at point based on the livestream that contains it.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code><span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-google-youtube-stream-offset</span> (time)
  <span class="org-doc">"Return the offset from the start of the stream.</span>
<span class="org-doc">When called interactively, copy it."</span>
  (<span class="org-keyword">interactive</span> (list (sacha-org-time-at-point)))
  (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> (stringp time)
             (string-match org-element&#45;&#45;timestamp-regexp time))
    (<span class="org-keyword">setq</span> time (org-timestamp-to-time (org-timestamp-from-string (match-string 0 time)))))
  (<span class="org-keyword">let</span> ((result
         (emacstv-format-seconds (sacha-google-youtube-live-seconds-offset-from-start-of-stream
                                  time))))
    (<span class="org-keyword">when</span> (called-interactively-p <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">any</span>)
      (kill-new result)
      (message <span class="org-string">"%s"</span> result))
    result))

(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-google-access-token</span> nil <span class="org-doc">"Cached access token."</span>)

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-google-access-token</span> ()
  <span class="org-doc">"Return Google access token."</span>
  (<span class="org-keyword">or</span> sacha-google-access-token
      (<span class="org-keyword">setq</span> sacha-google-access-token
            (string-trim (shell-command-to-string <span class="org-string">"gcloud auth application-default print-access-token"</span>)))))

(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-google-youtube-live-broadcasts</span> nil <span class="org-doc">"Cache."</span>)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-google-youtube-stream-offset-seconds</span> 10 <span class="org-doc">"Number of seconds to offset."</span>)

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-google-youtube-live-broadcasts</span> ()
  <span class="org-doc">"Return the list of broadcasts."</span>
  (<span class="org-keyword">or</span> sacha-google-youtube-live-broadcasts
      (<span class="org-keyword">setq</span> sacha-google-youtube-live-broadcasts
            (request-response-data
             (request <span class="org-string">"https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&amp;mine=true&amp;maxResults=10"</span>
               <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(format <span class="org-string">"Bearer %s"</span> (sacha-google-access-token))))
               <span class="org-builtin">:sync</span> t
               <span class="org-builtin">:parser</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">json-read</span>)))))

(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-google-youtube-live-get-broadcast-at-time</span> (time)
  <span class="org-doc">"Return the broadcast encompassing TIME."</span>
  (seq-find
   (<span class="org-keyword">lambda</span> (o)
     (<span class="org-keyword">or</span>
      <span class="org-comment-delimiter">;; </span><span class="org-comment">actual</span>
      (<span class="org-keyword">and</span>
       (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))
       (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualEndTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))
       (not (time-less-p time (date-to-time (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o)))))
       (time-less-p time (date-to-time (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualEndTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o)))))
      <span class="org-comment-delimiter">;; </span><span class="org-comment">actual, not done yet</span>
      (<span class="org-keyword">and</span>
       (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))
       (null (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualEndTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o)))
       (not (time-less-p time (date-to-time (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))))))
      <span class="org-comment-delimiter">;; </span><span class="org-comment">scheduled</span>
      (<span class="org-keyword">and</span>
       (null (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o)))
       (null (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualEndTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o)))
       (not (time-less-p time (date-to-time (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">scheduledStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))))))))
   (sort
    (seq-filter
     (<span class="org-keyword">lambda</span> (o)
       (<span class="org-keyword">or</span>
        (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))
        (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">scheduledStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))))
     (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">items</span>

                (sacha-google-youtube-live-broadcasts)))
    <span class="org-builtin">:key</span> (<span class="org-keyword">lambda</span> (o)
           (<span class="org-keyword">or</span>
            (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))
            (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">scheduledStartTime</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span> o))))
    <span class="org-builtin">:lessp</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string&lt;</span>)))

(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-google-youtube-live-seconds-offset-from-start-of-stream</span> (wall-time)
  <span class="org-doc">"Return number of seconds for WALL-TIME from the start of the stream that contains it.</span>
<span class="org-doc">Offset by `</span><span class="org-doc"><span class="org-constant">sacha-google-youtube-stream-offset-seconds</span></span><span class="org-doc">'."</span>
  (+ sacha-google-youtube-stream-offset-seconds
     (time-to-seconds
      (time-subtract
       wall-time
       (date-to-time
        (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">actualStartTime</span>
                   (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">snippet</span>
                              (sacha-google-youtube-live-get-broadcast-at-time wall-time))))))))

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-google-clear-cache</span> ()
  <span class="org-doc">"Clear cached Google access tokens and data."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">setq</span> sacha-google-access-token nil)
  (<span class="org-keyword">setq</span> sacha-google-youtube-live-broadcasts nil))
</code></pre>
</div>


<p>
For example:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(mapcar
 (<span class="org-keyword">lambda</span> (o)
   (list (concat
           <span class="org-string">"vtime:"</span>
           (sacha-google-youtube-stream-offset
            o))
         o))
 timestamps)
</code></pre>
</div>


<table>


<colgroup>
<col class="org-left">

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-left"><span class="media-time" data-start="1149.000">19:09</span></td>
<td class="org-left"><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T10:39:00-0400">[2026-04-16 Thu 10:39]</time></span>  Getting more out of livestreams</td>
</tr>

<tr>
<td class="org-left"><span class="media-time" data-start="2229.000">37:09</span></td>
<td class="org-left"><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T10:57:00-0400">[2026-04-16 Thu 10:57]</time></span>  Announcing livestreams</td>
</tr>

<tr>
<td class="org-left"><span class="media-time" data-start="2709.000">45:09</span></td>
<td class="org-left"><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T11:05:00-0400">[2026-04-16 Thu 11:05]</time></span>  Processing the recordings</td>
</tr>

<tr>
<td class="org-left"><span class="media-time" data-start="3069.000">51:09</span></td>
<td class="org-left"><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T11:11:00-0400">[2026-04-16 Thu 11:11]</time></span>  Non-packaged code</td>
</tr>
</tbody>
</table>

<p>
It's not exact, but it gets me in the right neighbourhood. Then I can use the MPV player to figure out a better timestamp if I want, and I can use my custom vtime Org link time to make those clickable when people have Javascript enabled. See <a href="https://sachachua.com/blog/2026/04/ye16-sacha-and-prot-talk-emacs/">YE16: Sacha and Prot talk Emacs</a> for examples.
</p>

<p>
It could be nice to log seconds someday for even finer timestamps. Still, this is handy already!
</p>
</div>
</div>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#streaming-make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2026/04/make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream/index.org">View Org source for this post</a></div><p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F04%2Fmake-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Org Mode: JS for translating times to people's local timezones</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2026/04/org-mode-js-for-translating-times-to-people-s-local-timezones/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2026-04-14T18:44:16Z</updated>
    <published>2026-04-14T18:44:16Z</published>
    <category term="org" />
<category term="emacs" />
<category term="js" />
		<id>https://sachachua.com/blog/2026/04/org-mode-js-for-translating-times-to-people-s-local-timezones/</id>
		<content type="html"><![CDATA[<p>
I want to get back into the swing of doing <a href="https://sachachua.com/topic/emacs-chat/">Emacs Chats</a> again, which means scheduling, which means timezones. Let's see first if anyone happens to match up with the Thursday timeslots (10:30 or 12:45) that I'd like to use for Emacs-y video things, but I might be able to shuffle things around if needed.
</p>

<p>
I want something that can translate times into people's local timezones.
I use Org Mode timestamps a lot because they're so easy to insert with <code>C-u C-c !</code> (<code>org-timestamp-inactive</code>), which inserts a timestamp like this:
</p>

<p>
<span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T10:30:00-0400">[2026-04-16 Thu 10:30]</time></span>
</p>

<p>
By default, the Org HTML export for it does not include the timezone offset. That's easily fixed by adding <code>%z</code> to the time specifier, like this:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">setq</span> org-html-datetime-formats <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"%F"</span> . <span class="org-string">"%FT%T%z"</span>))
</code></pre>
</div>


<p>
Now a little bit of Javascript code makes it clickable and lets us toggle a translated time. I put the time afterwards so that people can verify it visually. I never quite trust myself when it comes to timezone translations.
</p>


<div class="org-src-container">
<pre class="src src-js"><code><span class="org-keyword">function</span> <span class="org-function-name">translateTime</span>(<span class="org-variable-name">event</span>) {
  <span class="org-keyword">if</span> (event.target.getAttribute(<span class="org-string">'datetime'</span>)?.match(<span class="org-string">/[0-9][0-9][0-9][0-9]$/</span>)) {
    <span class="org-keyword">if</span> (event.target.querySelector(<span class="org-string">'.translated'</span>)) {
      event.target.querySelectorAll(<span class="org-string">'.translated'</span>).forEach((o) =&gt; o.remove());
    } <span class="org-keyword">else</span> {
      <span class="org-keyword">const</span> <span class="org-variable-name">span</span> = document.createElement(<span class="org-string">'span'</span>);
      span.classList.add(<span class="org-string">'translated'</span>);
      span.textContent = <span class="org-string">' &#8594; '</span> + (<span class="org-keyword">new</span> <span class="org-type">Date</span>(event.target.getAttribute(<span class="org-string">'datetime'</span>))).toLocaleString(<span class="org-constant">undefined</span>, {
        month: <span class="org-string">'short'</span>,  
        day: <span class="org-string">'numeric'</span>,  
        hour: <span class="org-string">'numeric'</span>, 
        minute: <span class="org-string">'2-digit'</span>,
        timeZoneName: <span class="org-string">'short'</span>
      });
      event.target.appendChild(span);
    }
  }
}
<span class="org-keyword">function</span> <span class="org-function-name">clickForLocalTime</span>() {
  document.querySelectorAll(<span class="org-string">'time'</span>).forEach((o) =&gt; {
    <span class="org-keyword">if</span> (o.getAttribute(<span class="org-string">'datetime'</span>)?.match(<span class="org-string">/[0-9][0-9][0-9][0-9]$/</span>)) {
      o.addEventListener(<span class="org-string">'click'</span>, translateTime);
      o.classList.add(<span class="org-string">'clickable'</span>);
    }
  });
}
</code></pre>
</div>


<p>
And some CSS to make it more obvious that it's now clickable:
</p>


<div class="org-src-container">
<pre class="src src-css"><code><span class="org-css-selector">.clickable</span> {
    <span class="org-css-property">cursor</span>: pointer;
    <span class="org-css-property">text-decoration</span>: underline dotted;
}
</code></pre>
</div>


<p>
Let's see if this is useful.
</p>

<p>
Someday, it would probably be handy to have a button that translates all the timestamps in a table, but this is a good starting point.
</p>
<div><a href="https://sachachua.com/blog/2026/04/org-mode-js-for-translating-times-to-people-s-local-timezones/index.org">View Org source for this post</a></div><p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F04%2Forg-mode-js-for-translating-times-to-people-s-local-timezones%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Org Mode: Tangle Emacs config snippets to different files and add boilerplate</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2026/04/org-mode-tangle-emacs-config-snippets-to-different-files-and-add-boilerplate/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2026-04-11T14:13:19Z</updated>
    <published>2026-04-11T14:13:19Z</published>
    <category term="emacs" />
<category term="org" />
		<id>https://sachachua.com/blog/2026/04/org-mode-tangle-emacs-config-snippets-to-different-files-and-add-boilerplate/</id>
		<content type="html"><![CDATA[<p>
I want to organize the functions in my Emacs configuration so that they are easier for me to test and so that other people can load them from my repository. Instead of copying multiple code blogs from my blog posts or my exported Emacs configuration, it would be great if people could just include a file from the repository. I don't think people copy that much from my config, but it might still be worth making it easier for people to borrow interesting functions. It would be great to have libraries of functions that people can evaluate without worrying about side effects, and then they can copy or write a shorter piece of code to use those functions.
</p>

<p>
<a href="https://protesilaos.com/emacs/dotemacs#h:fc1ea247-5ef6-4c4e-a807-6c7b2482af90">In Prot's configuration (The custom libraries of my configuration)</a>, he includes each library as in full, in a single code block, with the boilerplate description, keywords, and <code>(provide '...)</code> that make them more like other libraries in Emacs.
</p>

<p>
I'm not quite sure my little functions are at that point yet. For now, I like the way that the functions are embedded in the blog posts and notes that explain them, and the org-babel <code>:comments</code> argument can insert links back to the sections of my configuration that I can open with <code>org-open-at-point-global</code> or <code>org-babel-tangle-jump-to-org</code>.
</p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Thinking through the options...</strong></summary>
<p>
Org tangles blocks in order, so if I want boilerplate or if I want to add require statements, I need to have a section near the beginning of my config that sets those up for each file. Noweb references might help me with common text like the license. Likewise, if I want a <code>(provide ...)</code> line at the end of each file, I need a section near the end of the file.
</p>

<p>
If I want to specify things out of sequence, I could use Noweb. By setting <code>:noweb-ref some-id :tangle no</code> on the blocks I want to collect later, I can then tangle them in the middle of the boilerplate. Here's a brief demo:
</p>


<div class="org-src-container">
<pre class="src src-org"><code><span class="org-org-block-begin-line">#+begin_src emacs-lisp :noweb yes :tangle lisp/sacha-eshell.el :comments no</span>
<span class="org-org-block"><span class="org-comment-delimiter">;; </span></span><span class="org-org-block"><span class="org-comment">-*- lexical-binding: t; -*-</span></span>
<span class="org-org-block">&lt;&lt;sacha-eshell&gt;&gt;</span>
<span class="org-org-block">(</span><span class="org-org-block"><span class="org-keyword">provide</span></span><span class="org-org-block"> </span><span class="org-org-block"><span class="org-highlight-quoted-quote">'</span></span><span class="org-org-block"><span class="org-constant">sacha-eshell</span></span><span class="org-org-block">)</span>
<span class="org-org-block-end-line">#+end_src</span>
</code></pre>
</div>


<p>
However, I'll lose the comment links that let me jump back to the part of the Org file with the original source block. This means that if I use <code>find-function</code> to jump to the definition of a function and then I want to find the outline section related to it, I have to use a function that checks if this might be my custom code and then looks in my config for "defun &hellip;". It's a little less generic.
</p>

<p>
I wonder if I can combine multiple targets with some code that knows what it's being tangled to, so it can write slightly different text. <code>org-babel-tangle-single-block</code> currently calculates the result once and then adds it to the list for each filename, so that doesn't seem likely.
</p>

<p>
Alternatively, maybe I can use noweb or my own tangling function and add the link comments from org-babel-tangle-comments.
</p>


</details>

<p>
Aha, I can fiddle with <code>org-babel-post-tangle-hook</code> to insert the boilerplate after the blocks have been written. Then I can add the <code>lexical-binding: t</code> cookie and the structure that makes it look more like the other libraries people define and use. It's always nice when I can get away with a small change that uses an existing hook. For good measure, let's even include a list of links to the sections of my config that affect that file.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-dotemacs-url</span> <span class="org-string">"https://sachachua.com/dotemacs/"</span>)

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-dotemacs-link-for-section-at-point</span> (<span class="org-type">&amp;optional</span> combined)
  <span class="org-doc">"Return the link for the current section."</span>
  (<span class="org-keyword">let*</span> ((custom-id (org-entry-get-with-inheritance <span class="org-string">"CUSTOM_ID"</span>))
         (title (org-entry-get (point) <span class="org-string">"ITEM"</span>))
         (url (<span class="org-keyword">if</span> custom-id
                  (concat <span class="org-string">"dotemacs:"</span> custom-id)
                (concat sacha-dotemacs-url <span class="org-string">":-:text="</span> (url-hexify-string title)))))
    (<span class="org-keyword">if</span> combined
        (org-link-make-string
         url
         title)
      (cons url title))))

(<span class="org-keyword">eval-and-compile</span>
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">org-core</span> nil t)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">org-macs</span> nil t)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">org-src</span> nil t))
(<span class="org-keyword">declare-function</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-babel-tangle&#45;&#45;compute-targets</span> <span class="org-string">"ob-tangle"</span>)
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-org-collect-links-for-tangled-files</span> ()
  <span class="org-doc">"Return a list of ((filename (link link link link)) ...)."</span>
  (<span class="org-keyword">let*</span> ((file (buffer-file-name))
         results)
    (<span class="org-keyword">org-babel-map-src-blocks</span> (buffer-file-name)
      (<span class="org-keyword">let*</span> ((info (org-babel-get-src-block-info))
             (link (sacha-dotemacs-link-for-section-at-point)))
        (mapc
         (<span class="org-keyword">lambda</span> (target)
           (<span class="org-keyword">let</span> ((list (assoc target results <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>)))
             (<span class="org-keyword">if</span> list
                 (<span class="org-keyword">cl-pushnew</span> link (cdr list) <span class="org-builtin">:test</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">equal</span>)
               (<span class="org-keyword">push</span> (list target link) results))))
         (org-babel-tangle&#45;&#45;compute-targets file info))))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">Put it back in source order</span>
    (nreverse
     (mapcar (<span class="org-keyword">lambda</span> (o)
               (cons (car o)
                     (nreverse (cdr o))))
             results))))
(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-emacs-config-module-links</span> nil <span class="org-doc">"Cache for links from tangled files."</span>)

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-emacs-config-update-module-info</span> ()
  <span class="org-doc">"Update the list of links."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">setq</span> sacha-emacs-config-module-links
        (seq-filter
         (<span class="org-keyword">lambda</span> (o)
           (string-match <span class="org-string">"sacha-"</span> (car o)))
         (sacha-org-collect-links-for-tangled-files)))
  (<span class="org-keyword">setq</span> sacha-emacs-config-modules-info
        (mapcar (<span class="org-keyword">lambda</span> (group)
                  <span class="org-highlight-quoted-quote">`</span>(,(file-name-base (car group))
                    (commentary
                     .
                     ,(replace-regexp-in-string
                       <span class="org-string">"^"</span>
                       <span class="org-string">";; "</span>
                       (concat
                        <span class="org-string">"Related Emacs config sections:\n\n"</span>
                        (org-export-string-as
                         (mapconcat
                          (<span class="org-keyword">lambda</span> (link)
                            (concat <span class="org-string">"- "</span> (cdr link) <span class="org-string">"\\\\\n  "</span> (org-link-make-string (car link)) <span class="org-string">"\n"</span>))
                          (cdr group)
                          <span class="org-string">"\n"</span>)
                         <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">ascii</span>
                         t))))))
                sacha-emacs-config-module-links)))

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-emacs-config-prepare-to-tangle</span> ()
  <span class="org-doc">"Update module info if tangling my config."</span>
  (<span class="org-keyword">when</span> (string-match <span class="org-string">"Sacha.org"</span> (buffer-file-name))
    (sacha-emacs-config-update-module-info)))
</code></pre>
</div>


<p>
Let's set up the functions for tangling the boilerplate.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-emacs-config-modules-dir</span> <span class="org-string">"~/sync/emacs/lisp/"</span>)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-emacs-config-modules-info</span> nil <span class="org-doc">"Alist of module info."</span>)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-emacs-config-url</span> <span class="org-string">"https://sachachua.com/dotemacs"</span>)

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-org-babel-post-tangle-insert-boilerplate-for-sacha-lisp</span> ()
  (<span class="org-keyword">when</span> (file-in-directory-p (buffer-file-name) sacha-emacs-config-modules-dir)
    (goto-char (point-min))
    (<span class="org-keyword">let</span> ((base (file-name-base (buffer-file-name))))
      (insert (format <span class="org-string">";;; %s.el &#45;&#45;- %s -*- lexical-binding: t -*-</span>

<span class="org-string">;; Author: %s &lt;%s&gt;</span>
<span class="org-string">;; URL: %s</span>

<span class="org-string">;;; License:</span>
<span class="org-string">;;</span>
<span class="org-string">;; This file is not part of GNU Emacs.</span>
<span class="org-string">;;</span>
<span class="org-string">;; This is free software; you can redistribute it and/or modify</span>
<span class="org-string">;; it under the terms of the GNU General Public License as published by</span>
<span class="org-string">;; the Free Software Foundation; either version 3, or (at your option)</span>
<span class="org-string">;; any later version.</span>
<span class="org-string">;;</span>
<span class="org-string">;; This is distributed in the hope that it will be useful,</span>
<span class="org-string">;; but WITHOUT ANY WARRANTY; without even the implied warranty of</span>
<span class="org-string">;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the</span>
<span class="org-string">;; GNU General Public License for more details.</span>
<span class="org-string">;;</span>
<span class="org-string">;; You should have received a copy of the GNU General Public License</span>
<span class="org-string">;; along with GNU Emacs; see the file COPYING.  If not, write to the</span>
<span class="org-string">;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,</span>
<span class="org-string">;; Boston, MA 02110-1301, USA.</span>

<span class="org-string">;;; Commentary:</span>
<span class="org-string">;;</span>
<span class="org-string">%s</span>
<span class="org-string">;;; Code:</span>

<span class="org-string">\n\n"</span>
                      base
                      (<span class="org-keyword">or</span>
                       (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">description</span>
                                      (assoc-default base sacha-emacs-config-modules-info <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
                       <span class="org-string">""</span>)
                      user-full-name
                      user-mail-address
                      sacha-emacs-config-url
                      (<span class="org-keyword">or</span>
                       (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">commentary</span>
                                      (assoc-default base sacha-emacs-config-modules-info <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
                       <span class="org-string">""</span>)))
      (goto-char (point-max))
      (insert (format <span class="org-string">"\n(provide '%s)\n;;; %s.el ends here\n"</span>
                      base
                      base))
      (save-buffer))))
</code></pre>
</div>



<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">setq</span> sacha-emacs-config-url <span class="org-string">"https://sachachua.com/dotemacs"</span>)
(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org</span>
  (add-hook <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-babel-pre-tangle-hook</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">sacha-emacs-config-prepare-to-tangle</span>)
  (add-hook <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-babel-post-tangle-hook</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">sacha-org-babel-post-tangle-insert-boilerplate-for-sacha-lisp</span>))
</code></pre>
</div>


<p>
You can see the results at <a href="https://codeberg.org/sachac/.emacs.d/src/branch/gh-pages/lisp/">.emacs.d/lisp</a>. For example, the function definitions in this post are at <a href="https://codeberg.org/sachac/.emacs.d/src/branch/gh-pages/lisp/sacha-emacs.el">lisp/sacha-emacs.el</a>.
</p>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#org-mode-org-babel-tangling-sacha-emacs-config-snippets-to-different-files-and-adding-boilerplate">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2026/04/org-mode-tangle-emacs-config-snippets-to-different-files-and-add-boilerplate/index.org">View Org source for this post</a></div><p>You can <a href="https://sachachua.com/blog/2026/04/org-mode-tangle-emacs-config-snippets-to-different-files-and-add-boilerplate/#comment">view 2 comments</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F04%2Forg-mode-tangle-emacs-config-snippets-to-different-files-and-add-boilerplate%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">YE11: Fix find-function for Emacs Lisp from org-babel or scratch</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2026-04-05T21:03:48Z</updated>
    <published>2026-04-05T21:03:48Z</published>
    <category term="org" />
<category term="emacs" />
<category term="elisp" />
<category term="stream" />
<category term="yay-emacs" />
		<id>https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/</id>
		<content type="html"><![CDATA[<p>
<video controls="1" src="https://archive.org/download/yay-emacs-11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/ye11-find-function.mp4" poster="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/2026-04-05-19-25-03.png" type="video/mp4"><track kind="subtitles" label="Captions" src="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/Yay%20Emacs%2011:%20Fix%20find-function%20for%20Emacs%20Lisp%20from%20org-babel%20or%20scratch.vtt" srclang="en" default=""><span>Video not supported. Thumbnail:<br><img src="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/2026-04-05-19-25-03.png" alt="Thumbnail"></span></video>
</p>

<p>
<a href="https://archive.org/details/yay-emacs-11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch">Watch on Internet Archive</a>, <a href="https://youtube.com/live/PKkV1Tbev_Y">watch/comment on YouTube</a>, <a href="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/Yay%20Emacs%2011:%20Fix%20find-function%20for%20Emacs%20Lisp%20from%20org-babel%20or%20scratch.vtt">download captions</a>, or <a href="mailto:sacha@sachachua.com">email me</a>
</p>


<p>
Where can you define an Emacs Lisp function so
that you can use <code>find-function</code> to jump to it
again later?
</p>

<ul class="org-ul">
<li><b>A: In an indirect buffer</b> from Org Mode source
block with your favorite eval function like
<code>eval-defun</code> <label class="hint"><input type="checkbox"> <span class="hint-desc">(hint)</span><span class="hint-text">nope</span></label>

<ul class="org-ul">
<li><p>
<code>C-c '</code> (<code>org-edit-special</code>) inside the block; execute the defun with <code>C-M-x</code> (<code>eval-defun</code>), <code>C-x C-e</code> (<code>eval-last-sexp</code>), or <code>eval-buffer</code>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>    (<span class="org-keyword">defun</span> <span class="org-function-name">my-test-1</span> () (message <span class="org-string">"Hello"</span>))
</code></pre>
</div>
</li>
</ul></li>

<li><p>
<b>B: In an Org Mode file</b> by executing the block
with C-c C-c <label class="hint"><input type="checkbox"> <span class="hint-desc">(hint)</span><span class="hint-text">nope</span></label>
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>  (<span class="org-keyword">defun</span> <span class="org-function-name">my-test-2</span> () (message <span class="org-string">"Hello"</span>))
</code></pre>
</div>
</li>

<li><p>
<b>C: In a .el file</b> <label class="hint"><input type="checkbox"> <span class="hint-desc">(hint)</span><span class="hint-text">yup</span></label>
</p>

<p>
<a href="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/test-search-function.el">file:///tmp/test-search-function.el</a> : execute the defun with <code>C-M-x</code> (<code>eval-defun</code>), <code>C-x C-e</code> (<code>eval-last-sexp</code>), or <code>eval-buffer</code>
</p></li>

<li><p>
<b>D: In a scratch buffer,</b> other temporary buffer,
or really any buffer thanks to eval-last-sexp
<label class="hint"><input type="checkbox"> <span class="hint-desc">(hint)</span><span class="hint-text">nope</span></label>
</p>

<p>
<code>(defun my-test-4 () (message "Hello"))</code>
</p></li>
</ul>

<p>
Only option C works - it's gotta be in an .el file for
<code>find-function</code> to find it. But I love jumping to
function definitions using <code>find-function</code> or
<code>lispy-goto-symbol</code> (which is bound to <code>M-.</code> if
you use <a target="_blank" href="https://melpa.org/#/lispy">lispy</a> and set up <code>lispy-mode</code>) so
that I can look at or change how something works.
It can be a little frustrating when I try to jump
to a definition and it says, "Don't know where
blahblahblah is defined." I just defined it five
minutes ago! It's there in one of my other
buffers, don't make me look for it myself.
Probably this will get fixed in Emacs core
someday, but no worries, we can work around it
today with a little bit of advice.
</p>

<p>
I did some digging around in the source code.
Turns out that <code>symbol-file</code> can't find the
function definition in the <code>load-history</code> variable
if you're not in a .el file, so
<code>find-function-search-for-symbol</code> gets called with
<code>nil</code> for the library, which causes the error.
(<a href="https://github.com/emacs-mirror/emacs/blob/master/lisp/subr.el">emacs:subr.el</a>)
</p>

<p>
I wrote some advice that searches in any open
<code>emacs-lisp-mode</code> buffers or in a list of other
files, like my Emacs configuration.
This is how I activate it:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">setq</span> sacha-elisp-find-function-search-extra <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"~/sync/emacs/Sacha.org"</span>))
(advice-add <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">find-function-search-for-symbol</span> <span class="org-builtin">:around</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">sacha-elisp-find-function-search-for-symbol</span>)
</code></pre>
</div>


<p>
Now I should be able to jump to all those
functions wherever they're defined.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(my-test-1)
(my-test-2)
(my-test-3)
(my-test-4)
</code></pre>
</div>


<p>
Note that by default, <code>M-.</code> in <code>emacs-lisp-mode</code> uses <code>xref-find-definitions</code>, which seems to really want files. I haven't figured out a good workaround for that yet, but <a target="_blank" href="https://melpa.org/#/lispy">lispy-mode</a> makes <code>M-.</code> work and gives me a bunch of other great shortcuts, so I'd recommend checking that out.
</p>

<p>
Here's the source code for the find function thing:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-elisp-find-function-search-extra</span>
  nil
  <span class="org-doc">"List of filenames to search for functions."</span>)

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-elisp-find-function-search-for-symbol</span> (fn symbol type library <span class="org-type">&amp;rest</span> _)
  <span class="org-doc">"Find SYMBOL with TYPE in Emacs Lisp buffers or `</span><span class="org-doc"><span class="org-constant">sacha-find-function-search-extra</span></span><span class="org-doc">'.</span>
<span class="org-doc">Prioritize buffers that do not have associated files, such as Org Src</span>
<span class="org-doc">buffers or *scratch*. Note that the fallback search uses \"^([</span><span class="org-doc"><span class="org-negation-char">^</span></span><span class="org-doc"> )]+\" so that</span>
<span class="org-doc">it isn't confused by preceding forms.</span>

<span class="org-doc">If LIBRARY is specified, fall back to FN.</span>

<span class="org-doc">Activate this with:</span>

<span class="org-doc">(advice-add 'find-function-search-for-symbol</span>
<span class="org-doc"> :around #'sacha-org-babel-find-function-search-for-symbol-in-dotemacs)"</span>
  (<span class="org-keyword">if</span> (null library)
      <span class="org-comment-delimiter">;; </span><span class="org-comment">Could not find library; search my-dotemacs-file just in case</span>
      (<span class="org-keyword">progn</span>
        (<span class="org-keyword">while</span> (<span class="org-keyword">and</span> (symbolp symbol) (get symbol <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">definition-name</span>))
          (<span class="org-keyword">setq</span> symbol (get symbol <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">definition-name</span>)))
        (<span class="org-keyword">catch</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">found</span>
          (mapc
           (<span class="org-keyword">lambda</span> (buffer-or-file)
             (<span class="org-keyword">with-current-buffer</span> (<span class="org-keyword">if</span> (bufferp buffer-or-file)
                                      buffer-or-file
                                    (find-file-noselect buffer-or-file))
               (<span class="org-keyword">let*</span> ((regexp-symbol
                       (<span class="org-keyword">or</span> (<span class="org-keyword">and</span> (symbolp symbol)
                                (alist-get type (get symbol <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">find-function-type-alist</span>)))
                           (alist-get type find-function-regexp-alist)))
                      (form-matcher-factory
                       (<span class="org-keyword">and</span> (functionp (cdr-safe regexp-symbol))
                            (cdr regexp-symbol)))
                      (regexp-symbol (<span class="org-keyword">if</span> form-matcher-factory
                                         (car regexp-symbol)
                                       regexp-symbol))

                      (case-fold-search)
                      (regexp (<span class="org-keyword">if</span> (functionp regexp-symbol) regexp-symbol
                                (format (symbol-value regexp-symbol)
                                        <span class="org-comment-delimiter">;; </span><span class="org-comment">Entry for ` (backquote) macro in loaddefs.el,</span>
                                        <span class="org-comment-delimiter">;; </span><span class="org-comment">(defalias (quote \`)..., has a \ but</span>
                                        <span class="org-comment-delimiter">;; </span><span class="org-comment">(symbol-name symbol) doesn't.  Add an</span>
                                        <span class="org-comment-delimiter">;; </span><span class="org-comment">optional \ to catch this.</span>
                                        (concat <span class="org-string">"\\\\?"</span>
                                                (regexp-quote (symbol-name symbol)))))))
                 (<span class="org-keyword">save-restriction</span>
                   (widen)
                   (<span class="org-keyword">with-syntax-table</span> emacs-lisp-mode-syntax-table
                     (goto-char (point-min))
                     (<span class="org-keyword">if</span> (<span class="org-keyword">if</span> (functionp regexp)
                             (funcall regexp symbol)
                           (<span class="org-keyword">or</span> (re-search-forward regexp nil t)
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">`</span><span class="org-comment"><span class="org-constant">regexp</span></span><span class="org-comment">' matches definitions using known forms like</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">`</span><span class="org-comment"><span class="org-constant">defun</span></span><span class="org-comment">', or `</span><span class="org-comment"><span class="org-constant">defvar</span></span><span class="org-comment">'.  But some functions/variables</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">are defined using special macros (or functions), so</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">if `</span><span class="org-comment"><span class="org-constant">regexp</span></span><span class="org-comment">' can't find the definition, we look for</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">something of the form "(SOMETHING &lt;symbol&gt; ...)".</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">This fails to distinguish function definitions from</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">variable declarations (or even uses thereof), but is</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">a good pragmatic fallback.</span>
                               (re-search-forward
                                (concat <span class="org-string">"^([</span><span class="org-string"><span class="org-negation-char">^</span></span><span class="org-string"> )]+"</span> find-function-space-re <span class="org-string">"['(]?"</span>
                                        (regexp-quote (symbol-name symbol))
                                        <span class="org-string">"\\_&gt;"</span>)
                                nil t)))
                         (<span class="org-keyword">progn</span>
                           (beginning-of-line)
                           (<span class="org-keyword">throw</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">found</span>
                                   (cons (current-buffer) (point))))
                       (<span class="org-keyword">when-let*</span> ((find-expanded
                                    (<span class="org-keyword">when</span> (trusted-content-p)
                                      (find-function&#45;&#45;search-by-expanding-macros
                                       (current-buffer) symbol type
                                       form-matcher-factory))))
                         (<span class="org-keyword">throw</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">found</span>
                                 (cons (current-buffer)
                                       find-expanded)))))))))
           (delq nil
                 (append
                  (sort
                   (match-buffers <span class="org-highlight-quoted-quote">'</span>(derived-mode . emacs-lisp-mode))
                   <span class="org-builtin">:key</span> (<span class="org-keyword">lambda</span> (o) (<span class="org-keyword">or</span> (buffer-file-name o) <span class="org-string">""</span>)))
                  sacha-elisp-find-function-search-extra)))))
    (funcall fn symbol type library)))
</code></pre>
</div>


<p>
I even figured out how to <span title="(ignore (ert &quot;sacha-elisp&#45;&#45;find-function-search-for-symbol&#45;&#45;.*&quot;))">write tests for it</span>:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">ert-deftest</span> <span class="org-function-name">sacha-elisp&#45;&#45;find-function-search-for-symbol&#45;&#45;in-buffer</span> ()
  (<span class="org-keyword">let</span> ((sym (make-temp-name <span class="org-string">"&#45;&#45;test-fn"</span>))
        buffer)
    (<span class="org-keyword">unwind-protect</span>
        (<span class="org-keyword">with-temp-buffer</span>
          (emacs-lisp-mode)
          (insert (format <span class="org-string">";; Comment\n(defun %s () (message \"Hello\"))"</span> sym))
          (eval-last-sexp nil)
          (<span class="org-keyword">setq</span> buffer (current-buffer))
          (<span class="org-keyword">with-temp-buffer</span>
            (<span class="org-keyword">let</span> ((pos (sacha-elisp-find-function-search-for-symbol nil (intern sym) nil nil)))
              (<span class="org-keyword">should</span> (equal (car pos) buffer))
              (<span class="org-keyword">should</span> (equal (cdr pos) 12)))))
      (fmakunbound (intern sym)))))

(<span class="org-keyword">ert-deftest</span> <span class="org-function-name">sacha-elisp&#45;&#45;find-function-search-for-symbol&#45;&#45;in-file</span> ()
  (<span class="org-keyword">let*</span> ((sym (make-temp-name <span class="org-string">"&#45;&#45;test-fn"</span>))
         (temp-file (make-temp-file
                     <span class="org-string">"test-"</span> nil <span class="org-string">".org"</span>
                     (format
                      <span class="org-string">"#+begin_src emacs-lisp\n;; Comment\n(defun %s () (message \"Hello\"))\n#+end_src"</span>
                      sym)))
         (sacha-elisp-find-function-search-extra (list temp-file))
         buffer)
    (<span class="org-keyword">unwind-protect</span>
        (<span class="org-keyword">with-temp-buffer</span>
          (<span class="org-keyword">let</span> ((pos (sacha-elisp-find-function-search-for-symbol nil (intern sym) nil nil)))
            (<span class="org-keyword">should</span> (equal (buffer-file-name (car pos)) temp-file))
            (<span class="org-keyword">should</span> (equal (cdr pos) 35))))
      (delete-file temp-file))))
</code></pre>
</div>


<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#org-mode-org-babel-fix-find-function-when-i-ve-evaluated-something-from-org-babel">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/index.org">View Org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01KNFTFSC3D0K7XH1JW1XTF6QG" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F04%2Fye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Extract PDF highlights into an Org file with Python</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2026/04/demo-extract-pdf-highlights-into-an-org-file-with-python/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2026-04-07T02:55:57Z</updated>
    <published>2026-04-02T12:05:16Z</published>
    <category term="org" />
		<id>https://sachachua.com/blog/2026/04/demo-extract-pdf-highlights-into-an-org-file-with-python/</id>
		<content type="html"><![CDATA[<div class="update" id="orge9cc480">
<p>
<span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-06">[2026-04-06 Mon]</time></span>: Updated screenshot. I finished
reading all the pages and ended up with 202
highlights, so I'm going to have fun updating my
config with those notes!
</p>

</div>

<p>
I've been trying to find a good workflow for highlighting interesting parts of PDFs, and then getting that into my notes as images and text in Emacs. I think I've finally figured out something that works well for me that feels natural (marking things.
</p>

<p>
I wanted to read through <a href="https://protesilaos.com/emacs/dotemacs">Prot's Emacs configuration</a> while the kiddo played with her friends at the playground. I saved the web page as a PDF and exported it to <a href="http://getnoteful.com/">Noteful</a>. The PDF has 481 pages. Lots to explore! It was a bit chilly, so I had my gloves on. I used a capacitative stylus in my left hand to scroll the document and an Apple Pencil in my right hand to highlight the parts I wanted to add to my config or explore further.
</p>

<p>
Back at my computer, I used <code>pip install pymupdf</code> to install the <a href="https://github.com/pymupdf/PyMuPDF">PyMuPDF</a> library. I poked around the PDF in the Python shell to see what it had, and I noticed that the highlights were drawings with fill 0.5. So I wrote this Python script to extract the images and text near that rectangle:
</p>


<div class="org-src-container">
<pre class="src src-python"><code><span class="org-keyword">import</span> fitz
<span class="org-keyword">import</span> pathlib
<span class="org-keyword">import</span> sys
<span class="org-keyword">import</span> os

<span class="org-variable-name">BUFFER</span> <span class="org-operator">=</span> 5

<span class="org-keyword">def</span> <span class="org-function-name">extract_highlights</span>(filename, output_dir):
    <span class="org-variable-name">doc</span> <span class="org-operator">=</span> fitz.<span class="org-builtin">open</span>(filename)
    <span class="org-variable-name">s</span> <span class="org-operator">=</span> <span class="org-string">"* Excerpts</span><span class="org-constant">\n</span><span class="org-string">"</span>
    <span class="org-keyword">for</span> page_num, page <span class="org-keyword">in</span> <span class="org-builtin">enumerate</span>(doc):
        <span class="org-variable-name">page_width</span> <span class="org-operator">=</span> page.rect.width
        <span class="org-variable-name">page_text</span> <span class="org-operator">=</span> <span class="org-string">""</span>
        <span class="org-keyword">for</span> draw_num, d <span class="org-keyword">in</span> <span class="org-builtin">enumerate</span>(page.get_drawings()):
            <span class="org-keyword">if</span> d[<span class="org-string">'fill_opacity'</span>] <span class="org-operator">==</span> 0.5:
               <span class="org-variable-name">rect</span> <span class="org-operator">=</span> d[<span class="org-string">'rect'</span>]
               <span class="org-variable-name">clip_rect</span> <span class="org-operator">=</span> fitz.Rect(0, rect.y0 <span class="org-operator">-</span> BUFFER, page_width, rect.y1 <span class="org-operator">+</span> BUFFER)
               <span class="org-variable-name">img</span> <span class="org-operator">=</span> page.get_pixmap(clip<span class="org-operator">=</span>clip_rect)
               <span class="org-variable-name">img_filename</span> <span class="org-operator">=</span> <span class="org-string">"page-%03d-%d.png"</span> <span class="org-operator">%</span> (page_num <span class="org-operator">+</span> 1, draw_num <span class="org-operator">+</span> 1)
               img.save(os.path.join(output_dir, img_filename))
               <span class="org-variable-name">text</span> <span class="org-operator">=</span> page.get_text(clip<span class="org-operator">=</span>clip_rect)
               <span class="org-variable-name">page_text</span> <span class="org-operator">=</span> (page_text
                            <span class="org-operator">+</span> <span class="org-string">"[[file:%s]]</span><span class="org-constant">\n</span><span class="org-string">#+begin_quote</span><span class="org-constant">\n</span><span class="org-string">[[pdf:%s::%d][p%d]]: %s</span><span class="org-constant">\n</span><span class="org-string">#+end_quote</span><span class="org-constant">\n\n</span><span class="org-string">"</span>
                            <span class="org-operator">%</span> (img_filename,
                               os.path.join(<span class="org-string">".."</span>, filename),
                               page_num <span class="org-operator">+</span> 1,
                               page_num <span class="org-operator">+</span> 1, text))
        <span class="org-keyword">if</span> page_text <span class="org-operator">!=</span> <span class="org-string">""</span>:
            <span class="org-variable-name">s</span> <span class="org-operator">+=</span> <span class="org-string">"** Page %d</span><span class="org-constant">\n</span><span class="org-string">%s"</span> <span class="org-operator">%</span> (page_num <span class="org-operator">+</span> 1, page_text)
    pathlib.Path(os.path.join(output_dir, <span class="org-string">"index.org"</span>)).write_bytes(s.encode())

<span class="org-keyword">if</span> <span class="org-builtin">__name__</span> <span class="org-operator">==</span> <span class="org-string">'__main__'</span>:
    <span class="org-keyword">if</span> <span class="org-builtin">len</span>(sys.argv) <span class="org-operator">&lt;</span> 3:
        <span class="org-builtin">print</span>(<span class="org-string">"Usage: list-highlights.py pdf-filename output-dir"</span>)
    <span class="org-keyword">else</span>:
        extract_highlights(sys.argv[1], sys.argv[2])
</code></pre>
</div>


<p>
After I opened the resulting <code>index.org</code> file, I used <code>C-u C-u</code> <code>C-c C-x C-v</code> (<code>org-link-preview</code>) to make the images appear inline throughout the whole buffer. There's a little extra text from the PDF extraction, but it's a great starting point for cleaning up or copying. The org-pdftools package lets me link to specific pages in PDFs, neat!
</p>


<figure id="orgef7258d">
<a href="https://sachachua.com/blog/2026/04/demo-extract-pdf-highlights-into-an-org-file-with-python/2026-04-06-22-55-35.png"><img src="https://sachachua.com/blog/2026/04/demo-extract-pdf-highlights-into-an-org-file-with-python/2026-04-06-22-55-35.png" alt="2026-04-06-22-55-35.png"></a>

<figcaption><span class="figure-number">Figure 1: </span>Screenshot of Org Mode file with link previews</figcaption>
</figure>

<p>
To set up <code>org-pdftools</code>, I used:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">use-package</span> org-pdftools
  <span class="org-builtin">:hook</span> (org-mode . org-pdftools-setup-link))
</code></pre>
</div>


<p>
Here's my quick livestream about the script with a slightly older version that had an off-by-one bug in the page numbers and didn't have the fancy PDF links. =)
</p>

<p>
</p><div class="yt-video"><iframe width="456" height="315" title="YouTube video player" src="https://www.youtube-nocookie.com/embed/OTnYV2IZL_U?enablejsapi=1" frameborder="0" allowfullscreen="">nil</iframe><a href="https://youtube.com/live/OTnYV2IZL_U">Watch on YouTube</a></div>
<p></p>
<div><a href="https://sachachua.com/blog/2026/04/demo-extract-pdf-highlights-into-an-org-file-with-python/index.org">View Org source for this post</a></div><p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F04%2Fdemo-extract-pdf-highlights-into-an-org-file-with-python%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Categorizing Emacs News items by voice in Org Mode</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2026/03/categorizing-emacs-news-items-by-voice-in-org-mode/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2026-03-24T01:19:29Z</updated>
    <published>2026-03-24T01:19:29Z</published>
    <category term="speech" />
<category term="speech-recognition" />
<category term="emacs" />
<category term="org" />
		<id>https://sachachua.com/blog/2026/03/categorizing-emacs-news-items-by-voice-in-org-mode/</id>
		<content type="html"><![CDATA[<p>
I'm having fun exploring which things might actually be easier to do by voice than by typing. For example, after I wrote some code to <a href="https://sachachua.com/dotemacs#writing-and-editing-speech-recognition-expanding-yasnippet-by-voice">expand yasnippets by voice</a>, I realized that it was easier to:
</p>

<ol class="org-ol">
<li>press my shortcut,</li>
<li>say "okay, define interactive function",</li>
<li>and then press my shortcut again,</li>
</ol>

<p>
than to:
</p>

<ol class="org-ol">
<li>mentally say it,</li>
<li>get the first initials,</li>
<li>type in "dfi",</li>
<li>and press Tab to expand.</li>
</ol>

<p>
Another area where I do this kind of mental translation for keyboard shortcuts is when I categorize dozens of Emacs-related links each week for <a href="https://sachachua.com/blog/category/emacs-news">Emacs News</a>. I used to do this by hand. Then I wrote a function to try to guess the category based on regular expressions (<code>my-emacs-news-guess-category</code> in <a href="https://raw.githubusercontent.com/sachac/emacs-news/refs/heads/master/index.org">emacs-news/index.org</a>, which is large). Then I set up a menu that lets me <a href="https://sachachua.com/blog/2019/06/making-a-numpad-based-hydra-for-categorizing-org-list-items/">press numbers corresponding to the most frequent categories</a> and use tab completion for the rest. 1 is Emacs Lisp, 2 is Emacs development, 3 is Emacs configuration, 4 is appearance, 5 is navigation, and so on. It's not very efficient, but some of it has at least gotten into muscle memory, which is also part of why it's hard to change the mapping. I don't come across that many links for Emacs development or Spacemacs, and I could probably change them to something else, but&hellip; Anyway.
</p>


<figure id="org36cd391">
<a href="https://sachachua.com/blog/2026/03/categorizing-emacs-news-items-by-voice-in-org-mode/2026-03-23_20-38-33.png"><img src="https://sachachua.com/blog/2026/03/categorizing-emacs-news-items-by-voice-in-org-mode/2026-03-23_20-38-33.png" alt="2026-03-23_20-38-33.png"></a>

<figcaption><span class="figure-number">Figure 1: </span>Screenshot of my menu for categorizing links</figcaption>
</figure>

<p>
I wanted to see if I could categorize links by voice instead. I might not always be able to count on being able to type a lot, and it's always fun to experiment with other modes of input. Here's a demonstration showing how Emacs can automatically open the URLs, wait for voice input, and categorize the links using a reasonably close match. The <code>*Messages*</code> buffer displays the recognized output to help with debugging.
</p>

<div class="media-post" id="org09d244a">
<p>
</p><figure><video controls="1" src="https://sachachua.com/blog/2026/03/categorizing-emacs-news-items-by-voice-in-org-mode/2026-03-23-compressed.webm" type="video/webm"><a href="https://sachachua.com/blog/2026/03/categorizing-emacs-news-items-by-voice-in-org-mode/2026-03-23-compressed.webm">Download the video</a></video><figcaption><div>Screencast with audio: categorizing links by voice</div></figcaption></figure>
<p></p>

</div>

<p>
This is how it works:
</p>

<ol class="org-ol">
<li>It starts an <code>ffmpeg</code> recording process.</li>
<li>It starts <a href="https://github.com/snakers4/silero-vad">Silero voice activity detection</a>.</li>
<li>When it detects that speech has ended, it use <code>curl</code> to send the WAV to an OpenAI-compatible server (in my case, <a href="https://speaches.ai/">Speaches</a> with the <code>Systran/faster-whisper-base.en</code> model) for transcription, along with a prompt to try to influence the recognition.</li>
<li>It compares the result with the candidates using <code>string-distance</code> for an approximate match. It calls the code to move the current item to the right category, creating the category if needed.</li>
</ol>

<p>
Since this doesn't always result in the right match, I added an Undo command. I also have a Delete command for removing the current item, Scroll Up and Scroll Down, and a way to quit.
</p>
<div id="outline-container-categorizing-emacs-news-items-by-voice-in-org-mode-initial-thoughts" class="outline-3">
<h3 id="categorizing-emacs-news-items-by-voice-in-org-mode-initial-thoughts">Initial thoughts</h3>
<div class="outline-text-3" id="text-categorizing-emacs-news-items-by-voice-in-org-mode-initial-thoughts">
<p>
I used it to categorize lots of links in this week's Emacs News, and I think it's promising. I loved the way my hands didn't have to hover over the number keys or move between those and the characters. Using voice activity detection meant that I could just keep dictating categories instead of pressing keyboard shortcuts or using the foot pedal I recently dusted off. There's a slight delay, of course, but I think it's worth it. If this settles down and becomes a solid part of my workflow, I might even be able to knit or hand-sew while doing this step, or simply do some stretching exercises.
</p>

<p>
<b>What about using streaming speech recognition?</b> I've written some code to <a href="https://sachachua.com/dotemacs#writing-and-editing-speech-recognition-streaming-speech-recognition-into-emacs-using-google-chrome-web-speech-api">use streaming speech recognition</a>, but the performance wasn't good enough when I tried it on my laptop (Lenovo P52 released in 2018, no configured GPU under Linux). The streaming server dropped audio segments in order to try to catch up. I'd rather have everything transcribed at the level of the model I want, even if I have to wait a little while. I also tried <a href="https://sachachua.com/dotemacs/index.html#writing-and-editing-speech-recognition-streaming-speech-recognition-into-emacs-using-google-chrome-web-speech-api">using the Web Speech API in Google Chrome for real-time speech transcription</a>, but it's a little finicky. I'm happy with the performance I get from either <a href="https://sachachua.com/blog/2026/01/queue-multiple-transcriptions-with-whisper-el-speech-recognition/">manually queueing speech segments</a> or <a href="https://sachachua.com/blog/2026/01/using-silero-voice-activity-detection-to-automatically-queue-multiple-transcriptions-with-natrys-whisper-el/">using VAD</a> and then using batch speech recognition with a model that's kept in memory (which is why I use a local server instead of a command-line tool). Come to think of it, I should try this with a higher-quality model like medium or large, just in case the latency turns out to be not that much more for this use case.
</p>

<p>
<b>What about external voice control systems</b> like <a href="https://talonvoice.com/">Talon Voice</a> or <a href="https://www.cursorless.org/">Cursorless</a>? They seem like neat ideas and lots of people use them. I think hacking something into Emacs with full access to its internals could be lots of fun too.
</p>

<p>
A <i>lot</i> of people have experimented with voice input for Emacs over the years. It could be fun to pick up ideas for commands and grammars. Some examples:
</p>

<ul class="org-ul">
<li><a href="https://www.youtube.com/watch?v=8SkdfdXWYaI">Using Python to Code by Voice - YouTube</a> (2013)</li>
<li><a href="https://github.com/jgarvin/mandimus">jgarvin/mandimus: Use speech recognition to command your computer and Emacs. · GitHub</a></li>
<li><a href="https://github.com/ErikPrantare/cursorfree.el">ErikPrantare/cursorfree.el: Edit and navigate from anywhere in the buffer · GitHub</a></li>
<li><a href="https://git.sr.ht/~lepisma/emacs-speech-input/tree/master/item/README.org">~lepisma/emacs-speech-input</a> - uses the idea of a voice cursor, uses an LLM to execute editing instructions</li>
</ul>

<p>
<b>What about automating myself out of this loop?</b> I've considered training a classifier or sending the list to a large language model to categorize links in order to set more reasonable defaults, but I think I'd still want manual control, since the fun is in getting a sense of all the cool things that people are tinkering around with in the Emacs community. I found that with voice control, it was easier for me to say the category than to look for the category it suggested and then say "Okay" to accept the default. If I display the suggested category in a buffer with very large text (and possibly category-specific background colours), then I can quickly glance at it or use my peripheral vision. But yeah, it's probably easier to look at a page and say "Org Mode" than to look at the page, look at the default text, see if it matches Org Mode, and then say okay if it is.
</p>
</div>
</div>
<div id="outline-container-categorizing-emacs-news-items-by-voice-in-org-mode-ideas-for-next-steps" class="outline-3">
<h3 id="categorizing-emacs-news-items-by-voice-in-org-mode-ideas-for-next-steps">Ideas for next steps</h3>
<div class="outline-text-3" id="text-categorizing-emacs-news-items-by-voice-in-org-mode-ideas-for-next-steps">
<p>
I wonder how to line up several categories. I could probably rattle off a few without waiting for the next one to load, and just pause when I'm not sure. Maybe while there's a reasonably good match within the first 1-3 words, I'll take candidates from the front of the queue. Or I could delimit it with another easily-recognized word, like "next".
</p>

<p>
I want to make a more synchronous version of this idea so that I can have a speech-enabled drop-in replacement that I can use as my <code>y-or-n-p</code> while still being able to type <code>y</code> or <code>n</code>. This probably involves using <code>sit-for</code> and polling to see if it's done. And then I can use that to play Twenty Questions, but also to do more serious stuff. It would also be nice to have replacements for <code>read-string</code> and <code>completing-read</code>, since those block Emacs until the user enters something.
</p>

<p>
I might take a side-trip into a conversational interface for M-x doctor and M-x dunnet, because why not. Naturally, it also makes sense to voice-enable <a href="https://github.com/xenodium/agent-shell">agent-shell</a> and <a href="https://github.com/karthink/gptel">gptel</a> interactions.
</p>

<p>
I'd like to figure out a number- or word-based completion mechanism so that I can control Reddit link replacement as well, since I want to select from a list of links from the page. Maybe something similar to the way <a href="https://github.com/jcaw/voicemacs">voicemacs adds numbers to helm and company</a> or how <a href="http://www.cb1.com/~john/computing/emacs/handsfree/flexi-choose.html">flexi-choose.el</a> works.
</p>

<p>
I'm also thinking about how I can shift seamlessly between typing and speaking, like when I want to edit a link title. Maybe I can check if I'm in the minibuffer and what kind of minibuffer I'm in, perhaps like the way <a href="https://github.com/oantolin/embark">Embark</a> does.
</p>

<p>
It would be really cool to define speech commands by reusing the keymap structure that menus also use. This is how to <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Easy-Menu.html">define a menu</a> in Emacs Lisp:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">easy-menu-define</span> words-menu global-map
  <span class="org-doc">"Menu for word navigation commands."</span>
  <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"Words"</span>
     [<span class="org-string">"Forward word"</span> forward-word]
     [<span class="org-string">"Backward word"</span> backward-word]))
</code></pre>
</div>


<p>
and this is how to <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Modifying-Menus.html">set just one binding</a>:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(keymap-set-after my-menu <span class="org-string">"&lt;drink&gt;"</span>
  <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"Drink"</span> . drink-command) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">eat</span>)
</code></pre>
</div>


<p>
That makes sense to reuse for speech commands. I'd also like to be able to specify aliases while hiding them or collapsing them for a "What can I say" help view&hellip; Also, if keymaps work, then maybe minor modes or transient maps could work? This sort of feels like it should be the voice equivalent of a transient map.
</p>
</div>
</div>
<div id="outline-container-categorizing-emacs-news-items-by-voice-in-org-mode-the-code-so-far" class="outline-3">
<h3 id="categorizing-emacs-news-items-by-voice-in-org-mode-the-code-so-far">The code so far</h3>
<div class="outline-text-3" id="text-categorizing-emacs-news-items-by-voice-in-org-mode-the-code-so-far">

<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-emacs-news-categorize-with-voice</span> (<span class="org-type">&amp;optional</span> skip-browse)
  (<span class="org-keyword">interactive</span> (list current-prefix-arg))
  (<span class="org-keyword">unless</span> skip-browse
    (my-spookfox-browse))
  (speech-input-cancel-recording)
  (<span class="org-keyword">let</span> ((default (<span class="org-keyword">if</span> (fboundp <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-emacs-news-guess-category</span>) (my-emacs-news-guess-category))))
    (speech-input-from-list
     (<span class="org-keyword">if</span> default
         (format <span class="org-string">"Category (%s): "</span> default)
       <span class="org-string">"Category: "</span>)
     <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"Org Mode"</span> <span class="org-string">"Org"</span> <span class="org-string">"Org Mode"</span>)
       <span class="org-string">"Other"</span>
       <span class="org-string">"Emacs Lisp"</span>
       <span class="org-string">"Coding"</span>
       (<span class="org-string">"Emacs configuration"</span> <span class="org-string">"Config"</span> <span class="org-string">"Configuration"</span>)
       (<span class="org-string">"Appearance"</span> <span class="org-string">"Appearance"</span>)
       (<span class="org-string">"Default"</span> <span class="org-string">"Okay"</span> <span class="org-string">"Default"</span>)
       <span class="org-string">"Community"</span>
       <span class="org-string">"AI"</span>
       <span class="org-string">"Writing"</span>
       (<span class="org-string">"Reddit"</span> <span class="org-string">"Read it"</span> <span class="org-string">"Reddit"</span>)
       <span class="org-string">"Shells"</span>
       <span class="org-string">"Navigation"</span>
       <span class="org-string">"Fun"</span>
       (<span class="org-string">"Dired"</span> <span class="org-string">"Directory"</span> <span class="org-string">"Dir ed"</span>)
       (<span class="org-string">"Mail, news, and chat"</span> <span class="org-string">"News"</span> <span class="org-string">"Mail"</span> <span class="org-string">"Chat"</span>)
       <span class="org-string">"Multimedia"</span>
       <span class="org-string">"Scroll down"</span>
       <span class="org-string">"Scroll up"</span>
       <span class="org-string">"Web"</span>
       <span class="org-string">"Delete"</span>
       <span class="org-string">"Skip"</span>
       <span class="org-string">"Undo"</span>
       (<span class="org-string">"Quit"</span> <span class="org-string">"Quit"</span> <span class="org-string">"Cancel"</span> <span class="org-string">"All done"</span>))
     (<span class="org-keyword">lambda</span> (result text)
       (message <span class="org-string">"Recognized %s original %s"</span> result text)
       (<span class="org-keyword">pcase</span> result
         (<span class="org-string">"Undo"</span>
          (undo)
          (my-emacs-news-categorize-with-voice t))
         (<span class="org-string">"Skip"</span>
          (forward-line)
          (my-emacs-news-categorize-with-voice))
         (<span class="org-string">"Quit"</span>
          (message <span class="org-string">"All done."</span>)
          (speech-input-cancel-recording))
         (<span class="org-string">"Reddit"</span>
          (my-emacs-news-replace-reddit-link)
          (my-emacs-news-categorize-with-voice t))
         (<span class="org-string">"Scroll down"</span>
          (my-spookfox-scroll-down)
          (my-emacs-news-categorize-with-voice t))
         (<span class="org-string">"Scroll up"</span>
          (my-spookfox-scroll-up)
          (my-emacs-news-categorize-with-voice t))
         (<span class="org-string">"Delete"</span>
          (delete-line)
          (undo-boundary)
          (my-emacs-news-categorize-with-voice))
         (<span class="org-string">"Default"</span>
          (my-org-move-current-item-to-category
           (concat default <span class="org-string">":"</span>))
          (undo-boundary)
          (my-emacs-news-categorize-with-voice))
         (_
          (my-org-move-current-item-to-category
           (concat result <span class="org-string">":"</span>))
          (undo-boundary)
          (my-emacs-news-categorize-with-voice))))
     t)))
</code></pre>
</div>


<p>
It uses <a href="https://sachachua.com/dotemacs#spookfox">Spookfox to control Firefox from Emacs</a>:
</p>

<p>

</p><div class="org-src-container">
<pre class="src src-emacs-lisp" id="org22d90d7"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-spookfox-scroll-down</span> ()
  (<span class="org-keyword">interactive</span>)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"window.scrollBy(0, document.documentElement.clientHeight);"</span> t))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-spookfox-scroll-up</span> ()
  (<span class="org-keyword">interactive</span>)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"window.scrollBy(0, -document.documentElement.clientHeight);"</span>))
</code></pre>
</div>


<p></p>

<p>

</p><div class="org-src-container">
<pre class="src src-emacs-lisp" id="orga82aa0d"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-spookfox-background-tab</span> (url <span class="org-type">&amp;rest</span> args)
  <span class="org-doc">"Open URL as a background tab."</span>
  (<span class="org-keyword">if</span> spookfox&#45;&#45;connected-clients
      (spookfox-tabs&#45;&#45;request (cl-first spookfox&#45;&#45;connected-clients) <span class="org-string">"OPEN_TAB"</span> <span class="org-highlight-quoted-quote">`</span>(<span class="org-builtin">:url</span> ,url))
    (browse-url url)))
</code></pre>
</div>


<p></p>

<p>
It also uses these functions for <a href="https://sachachua.com/dotemacs#digital-index-piles-with-emacs">categorizing Org Mode items</a>:
</p>

<p>

</p><div class="org-src-container">
<pre class="src src-emacs-lisp" id="org955687d"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-move-current-item-to-category</span> (category)
    <span class="org-doc">"Move current list item under CATEGORY earlier in the list.</span>
<span class="org-doc">  CATEGORY can be a string or a list of the form (text indent regexp).</span>
<span class="org-doc">  Point should be on the next line to process, even if a new category</span>
<span class="org-doc">  has been inserted."</span>
    (<span class="org-keyword">interactive</span> (list (completing-read <span class="org-string">"Category: "</span> (my-org-get-list-categories))))
    (<span class="org-keyword">when</span> category
      (<span class="org-keyword">let*</span> ((col (current-column))
             (item (point-at-bol))
             (struct (org-list-struct))
             (category-text (<span class="org-keyword">if</span> (stringp category) category (elt category 0)))
             (category-indent (<span class="org-keyword">if</span> (stringp category) 2 (+ 2 (elt category 1))))
             (category-regexp (<span class="org-keyword">if</span> (stringp category) category (elt category 2)))
             (end (elt (car (last struct)) 6))
             (pos (point))
             s)
        (<span class="org-keyword">setq</span> s (org-remove-indentation (buffer-substring-no-properties item (org-list-get-item-end item struct))))
        (<span class="org-keyword">save-excursion</span>
          (<span class="org-keyword">if</span> (string= category-text <span class="org-string">"x"</span>)
              (org-list-send-item item <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">delete</span> struct)
            (goto-char (caar struct))
            (<span class="org-keyword">if</span> (re-search-forward (concat <span class="org-string">"^ *- +"</span> category-regexp) end t)
                (<span class="org-keyword">progn</span>
                  <span class="org-comment-delimiter">;; </span><span class="org-comment">needs a patch to ol.el to check if stringp</span>
                  (org-list-send-item item (point-at-bol) struct)
                  (org-move-item-down)
                  (org-indent-item))
              (goto-char end)
              (org-list-insert-item
               (point-at-bol)
               struct (org-list-prevs-alist struct))
              (<span class="org-keyword">let</span> ((old-struct (copy-tree struct)))
                (org-list-set-ind (point-at-bol) struct 0)
                (org-list-struct-fix-bul struct (org-list-prevs-alist struct))
                (org-list-struct-apply-struct struct old-struct))
              (goto-char (point-at-eol))
              (insert category-text)
              (org-list-send-item item <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">end</span> struct)
              (org-indent-item)
              (org-indent-item))
            (recenter))))))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-guess-list-category</span> (<span class="org-type">&amp;optional</span> categories)
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">cl-lib</span>)
  (<span class="org-keyword">unless</span> categories
    (<span class="org-keyword">setq</span> categories
          (my-helm-org-list-categories-init-candidates)))
  (<span class="org-keyword">let*</span> ((beg (line-beginning-position))
         (end (line-end-position))
         (string (buffer-substring-no-properties beg end))
         (found
          (cl-member string
                     categories
                     <span class="org-builtin">:test</span>
                     (<span class="org-keyword">lambda</span> (string cat-entry)
                       (<span class="org-keyword">unless</span> (string= (car cat-entry) <span class="org-string">"x"</span>)
                         (string-match (regexp-quote (downcase (car cat-entry)))
                                       string))))))
    (<span class="org-keyword">when</span> (car found)
      (my-org-move-current-item-to-category
       (cdr (car found)))
      t)))
</code></pre>
</div>


<p></p>

<p>
For the <code>speech-input</code> functions, experimental code is at <a href="https://codeberg.org/sachac/speech-input">https://codeberg.org/sachac/speech-input</a> .
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2026/03/categorizing-emacs-news-items-by-voice-in-org-mode/index.org">View Org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01KMER8FH7NZT56ENT0NHE36Q0" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F03%2Fcategorizing-emacs-news-items-by-voice-in-org-mode%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry>
</feed>