<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/rss.xsl" type="text/xsl"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"

>
<channel>
	<title>Sacha Chua - category - org</title>
	<atom:link href="https://sachachua.com/blog/category/org/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/org" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/org/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
  
	<lastBuildDate>Mon, 29 Jun 2026 17:04:18 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>From DC Toedt: Copy Org Mode as Markdown</title>
		<link>https://sachachua.com/blog/2026/06/from-dc-toedt-copy-org-mode-as-markdown/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 09 Jun 2026 15:49:08 GMT</pubDate>
    <category>emacs</category>
<category>org</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/06/from-dc-toedt-copy-org-mode-as-markdown/</guid>
		<description><![CDATA[<div class="update" id="org6a372d6">
<p>
<span class="timestamp-wrapper"><time class="timestamp" datetime="2026-06-10">[2026-06-10 Wed]</time></span>: Add embark way to do things.
</p>

</div>

<p>
DC Toedt is a lawyer and professor of practice who uses Emacs and Org Mode. He wanted a small Emacs Lisp function to convert Org Mode syntax to Markdown and copy it to the clipboard to make it easier to copy the materials he's writing for a course on contract drafting. This seems to be a common need, and here are several other approaches:
</p>

<ul class="org-ul">
<li><code>embark-org-copy-as-markdown</code> in <a target="_blank" href="https://melpa.org/#/embark">embark</a></li>
<li><a href="https://mbork.pl/2021-05-02_Org-mode_to_Markdown_via_the_clipboard">Marcin Borkowski: 2021-05-02 Org-mode to Markdown via the clipboard</a></li>
<li><a href="https://mmk2410.org/2026/04/14/copy-an-org-mode-region-as-markdown">Marcel Kapfer - Copy an Org Mode region as Markdown</a></li>
<li><a href="https://www.reddit.com/r/emacs/comments/17um2fk/does_anyone_have_a_function_they_use_to_quickly/">Reddit</a></li>
<li><a href="https://www.reddit.com/r/emacs/comments/e98yyf/send_output_of_orgmdexportasmarkdownstraight_to/">Reddit</a></li>
</ul>

<p>
Anyway, DC shared how he used Claude to generate a simple function to do it, which is here under public domain:
</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/org-to-markdown-clipboard</span> ()
  <span class="org-doc">"Export org region (or buffer) to Markdown and copy to clipboard.</span>
<span class="org-doc">With no active region, exports the whole buffer."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">ox-md</span>)
  (<span class="org-keyword">let*</span> ((text (<span class="org-keyword">if</span> (use-region-p)
                   (buffer-substring-no-properties (region-beginning)
(region-end))
                 (buffer-substring-no-properties (point-min) (point-max))))
         (md (org-export-string-as text <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">md</span> t <span class="org-highlight-quoted-quote">'</span>(<span class="org-builtin">:with-toc</span> nil
                                                <span class="org-builtin">:with-author</span> nil
                                                <span class="org-builtin">:with-date</span> nil
                                                <span class="org-builtin">:with-title</span> nil))))
    (kill-new md)
    (message <span class="org-string">"Markdown copied (%d chars)"</span> (length md))))
(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org</span>
  (define-key org-mode-map (kbd <span class="org-string">"C-c m"</span>) <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my/org-to-markdown-clipboard</span>))
</code></pre>
</div>

<div><a href="https://sachachua.com/blog/2026/06/from-dc-toedt-copy-org-mode-as-markdown/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%2F06%2Ffrom-dc-toedt-copy-org-mode-as-markdown%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>]]></description>
		</item><item>
		<title>Create a Google Calendar event from an Org Mode timestamp</title>
		<link>https://sachachua.com/blog/2026/04/create-a-google-calendar-event-from-an-org-mode-timestamp/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 13:26:41 GMT</pubDate>
    <category>org</category>
<category>emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/04/create-a-google-calendar-event-from-an-org-mode-timestamp/</guid>
		<description><![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>]]></description>
		</item><item>
		<title>Make chapter markers and video time hyperlinks easier to note while I livestream</title>
		<link>https://sachachua.com/blog/2026/04/make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 04:27:43 GMT</pubDate>
    <category>org</category>
<category>emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/04/make-chapter-markers-and-video-time-hyperlinks-easier-to-note-while-i-livestream/</guid>
		<description><![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/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/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>]]></description>
		</item><item>
		<title>Org Mode: JS for translating times to people's local timezones</title>
		<link>https://sachachua.com/blog/2026/04/org-mode-js-for-translating-times-to-people-s-local-timezones/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 14 Apr 2026 18:44:16 GMT</pubDate>
    <category>org</category>
<category>emacs</category>
<category>js</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/04/org-mode-js-for-translating-times-to-people-s-local-timezones/</guid>
		<description><![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>]]></description>
		</item><item>
		<title>Org Mode: Tangle Emacs config snippets to different files and add boilerplate</title>
		<link>https://sachachua.com/blog/2026/04/org-mode-tangle-emacs-config-snippets-to-different-files-and-add-boilerplate/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sat, 11 Apr 2026 14:13:19 GMT</pubDate>
    <category>emacs</category>
<category>org</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/04/org-mode-tangle-emacs-config-snippets-to-different-files-and-add-boilerplate/</guid>
		<description><![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>]]></description>
		</item><item>
		<title>YE11: Fix find-function for Emacs Lisp from org-babel or scratch</title>
		<link>https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 05 Apr 2026 21:03:48 GMT</pubDate>
    <category>org</category>
<category>emacs</category>
<category>elisp</category>
<category>stream</category>
<category>yay-emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/</guid>
		<description><![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>]]></description>
		</item><item>
		<title>Extract PDF highlights into an Org file with Python</title>
		<link>https://sachachua.com/blog/2026/04/demo-extract-pdf-highlights-into-an-org-file-with-python/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 02 Apr 2026 12:05:16 GMT</pubDate>
    <category>org</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/04/demo-extract-pdf-highlights-into-an-org-file-with-python/</guid>
		<description><![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>]]></description>
		</item>
	</channel>
</rss>