<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~files/atom-premium.xsl"?>
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:feedpress="https://feed.press/xmlns" xmlns:media="http://search.yahoo.com/mrss/" xmlns:podcast="https://podcastindex.org/namespace/1.0">
  <feedpress:locale>en</feedpress:locale>
  <feedpress:newsletterId>brettterpstra</feedpress:newsletterId>
  <link rel="hub" href="https://feedpress.superfeedr.com/"/>
  <logo>https://static.feedpress.com/logo/brettterpstra-5d7926fa5332c.png</logo>
  <link rel="alternate" type="text/html" href="https://brettterpstra.com"/>
  <subtitle>Welcome to The Lab, detailing the coding and automation exploits of Brett Terpstra.</subtitle>
  <title>BrettTerpstra.com - The Mad Science of Brett Terpstra</title>
  <link href="https://brett.trpstra.net/" rel="self"/>
  <link href="https://brettterpstra.com/"/>
  <updated>2026-03-31T08:52:06-05:00</updated>
  <id>https://brettterpstra.com/</id>
  <author>
    <name><![CDATA[Brett Terpstra]]></name>
  </author>
  <generator uri="https://brettterpstra.com">BrettTerpstra</generator>
  <entry>
    <title type="html"><![CDATA[Web Excursions for March 31st, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17310444/web-excursions-for-march-31st-2026"/>
    <updated>2026-03-31T07:31:00-05:00</updated>
    <published>2026-03-31T07:31:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/31/web-excursions-for-march-31st-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with Setapp. <a href="https://go.setapp.com/stp44">Get access to hundreds of Mac and iOS apps for one low monthly subscription fee.</a></p>

<dl>
  <dt><a href="https://thecause.substack.com/p/the-worst-decline-of-democracy-ever?__readwiseLocation">The worst decline of democracy ever recorded could have been far worse.</a></dt>
  <dd>
    <blockquote>
      <p>The institutions that were supposed to be the brakes either surrendered or stumbled over each other. The people put themselves in the gears.</p>
    </blockquote>
  </dd>
  <dd>
    <p>I don&rsquo;t make this blog very political, which is odd given how political I am in my personal life. But this shouldn&rsquo;t be a partisan post. This is about all of us.</p>
  </dd>
  <dt><a href="https://kennethreitz.org/essays/2026-03-18-open_source_gave_me_everything_until_i_had_nothing_left_to_give?__readwiseLocation=">Open Source Gave Me Everything Until I Had Nothing Left to Give - Kenneth Reitz</a></dt>
  <dd>Not a perfect parallel to my own experience (I&rsquo;ve never had a top open source project or gotten into keynoting) but this rings very familiar to me. My acclaimed &ldquo;productivity&rdquo; has been the result of some very trying bipolar episodes that plagued me for years, leading to burnout.</dd>
  <dt><a href="https://doing.aaronmallen.dev/">doing in Rust</a></dt>
  <dd>A full port of my Ruby project, Doing, to Rust. Coming along nicely.</dd>
  <dd>
    <blockquote>
      <p>A command line tool for remembering what you were doing and tracking what you&rsquo;ve done — stored as plain text, versioned with your project.</p>
    </blockquote>
  </dd>
  <dt><a href="https://trystenofast.today/">Steno — Your keyboard slows you down.</a></dt>
  <dd>
    <blockquote>
      <p>Steno is a native macOS voice-to-text app. Hold a key, speak, release — text appears instantly in any app. Sub-second transcription, voice commands, text snippets, and dictation history. Built with Swift for speed and accuracy.</p>
    </blockquote>
  </dd>
  <dd>
    <p>Impressive AI-driven dictation with great privacy features and local-only mode (Apple Intelligence).</p>
  </dd>
  <dt><a href="https://www.joshholtz.com/blog/2021/06/23/automating-ios-shortcuts-the-cron-job-way.html">Automating iOS Shortcuts - The Cron Job Way</a></dt>
  <dd>
    <blockquote>
      <p>Today’s unnecessary and over-engineered thing is automating iOS (and macOS) Shortcuts using the same syntax that’s used to schedule cron jobs.</p>
    </blockquote>
  </dd>
</dl>

<p><a href="https://go.setapp.com/stp44">Check out Setapp</a> today and get access to the best Mac and iOS apps out there.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116323869793689806">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+March+31st%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F31%2Fweb-excursions-for-march-31st-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F31%2Fweb-excursions-for-march-31st-2026%2F&text=Web+Excursions+for+March+31st%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F31%2Fweb-excursions-for-march-31st-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17310444.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[My ultimate keyboard-driven Mac utility list]]></title>
    <link href="https://brett.trpstra.net/link/535/17307461/my-ultimate-keyboard-driven-mac-utility-list"/>
    <updated>2026-03-27T06:51:00-05:00</updated>
    <published>2026-03-27T06:51:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/27/my-ultimate-keyboard-driven-mac-utility-list</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/keyboard-header-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>The keyboard is the most powerful tool we have when using our Macs, which is why I&rsquo;ve curated what might seem like an overwhelming combination of keyboard utilities that I use daily.</p>

<p>Each of these tools enhances my productivity in unique ways, so whether you&rsquo;re looking to streamline your workflow or just want to explore some interesting keyboard hacks, I hope the following links and descriptions can help you find something valuable.</p>

<h3 id="keyboard-navigation">Keyboard Navigation</h3>

<p>When it comes to navigating my system with speed and precision, a few key utilities stand out. I use <strong><a href="https://superkey.app/">Superkey</a></strong> to enhance my existing keyboard shortcuts, making application navigation smoother and more efficient. It searches text on the screen and simulates a mouse click anywhere there&rsquo;s matching text. It allows me to access features and functions without ever taking my fingers off the keyboard. For those looking for a similar solution, <strong><a href="https://wooshy.app/">Wooshy</a></strong> offers a compelling alternative, providing seamless keyboard control for navigating all UI elements in an app.</p>

<p>Another indispensable tool in my toolkit is <strong><a href="https://kindavim.app/">KindaVim</a></strong>, which brings Vim-like key bindings to any application, allowing me to navigate text with great efficiency. It’s perfect for those who appreciate the keyboard-centric ease of Vim.</p>

<p>KindaVim isn&rsquo;t perfect, but it&rsquo;s constantly improving. But as an example, if I use <span class="keycombo separated" title="V"><kbd class="key symbol">V</kbd></span> and <span class="keycombo separated" title="H/j/k/l"><kbd class="key symbol">h/j/k/l</kbd></span> in MultiMarkdown Composer to make a selection, when I hit <span class="keycombo separated" title="X"><kbd class="key symbol">X</kbd></span> to kill the selection, the cursor jumps up a few paragraphs. It&rsquo;s a bit jarring, but for basic Vim navigation like <span class="keycombo separated" title="H/j/k/l/u/d/a/i"><kbd class="key symbol">h/j/k/l/u/d/a/i</kbd></span>, etc., it&rsquo;s perfect, and enhances every app I write in.</p>

<p>There are always keyboard shortcuts for common menu items in any Mac app. I frequently access menu bars with <span class="keycombo separated" title="Shift-Command-?"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">?</kbd></span> and use the help field to find menu items, and try to memorize ones that have the keyboard shortcut indicated on them. I have an as-of-yet-unpublished script based on <a href="https://brettterpstra.com/2026/02/15/niftymenu-tahoe-edition/">NiftyMenu</a> that scans any app for every available shortcut and generates <a href="https://kapeli.com/dash">Dash</a> cheatsheets or <a href="https://brettterpstra.com/projects/cheaters/">Cheaters</a> pages for it.</p>

<p>Speaking of menu item keyboard shortcuts, <a href="https://appmakes.io/paletro">Paletro</a> is amazing. It gives me a <span class="keycombo separated" title="Shift-Command-P"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">P</kbd></span> shortcut in any app, with fuzzy type-ahead searching for any menu item. I use it daily.</p>

<p>Lastly, <strong><a href="https://scrolla.app/">Scrolla</a></strong> is a game-changer for scrolling through documents and long text, enabling me to keep my hands on the keyboard whenever I need to go through lengthy content.</p>

<h3 id="keyboard-modifications">Keyboard Modifications</h3>

<p>For deeper customization, I rely on a mix of tools that modify key functions and introduce new shortcuts.</p>

<p><strong><a href="https://folivora.ai/">BetterTouchTool</a></strong> is a powerhouse for managing trackpad gestures and keyboard shortcuts, letting me create personalized commands that fit my workflow perfectly.</p>

<p><strong><a href="https://karabiner-elements.pqrs.org/">Karabiner-Elements</a></strong> takes it a step further with its ability to remap keys at the system level, modifying their behavior. I’ve set up my Hyper key using this tool, which unlocks numerous possibilities for shortcuts and personalizations (check out <a href="https://brettterpstra.com/karabiner/">my Karabiner mods</a> for some advanced tweaks).</p>

<p>Additionally, my <strong><a href="https://brettterpstra.com/projects/keybindings/">DefaultKeyBindings.dict</a></strong> file is essential for crafting custom keyboard shortcuts tailored to my preferences. MacOS&rsquo;s built-in key mapping features allow for sophisticated combinations that further enhance my efficiency. Coupled with <strong><a href="https://www.keyboardmaestro.com/">Keyboard Maestro</a></strong>, which automates repetitive tasks and creates amazing automations, I&rsquo;m able to truly streamline my text editing.</p>

<h3 id="launchers">Launchers</h3>

<p>When it comes to launching apps and managing my clipboard, <strong><a href="https://www.obdev.at/launchbar">LaunchBar</a></strong> reigns supreme in my daily workflow. I’ve experimented with every launcher out there, but nothing has stuck quite like LaunchBar. Its intuitive interface complements my muscle memory, allowing me to access apps, files, and even perform quick calculations without losing focus.</p>

<p>With LaunchBar, I can access my entire clipboard history across reboots, copying things back into the pasteboard or pasting them directly. And I can double-tap the option key to &ldquo;Instant Send&rdquo; any selected file to LaunchBar, then hit tab to open it in an app or perform a custom action on it. Combined with KindaVim&rsquo;s ability to navigate Finder with ease and SuperKey&rsquo;s ability to select any file I can see on my screen, it&rsquo;s crazy how useful LaunchBar is.</p>

<p>Though <strong><a href="https://github.com/mikker/LeaderKey">LeaderKey</a></strong> might seem redundant alongside LaunchBar, it offers the benefit of static, single-key sequences for launching frequently used applications and URLs. This has also become a part of my muscle memory, enabling quick access without sifting through multiple options. Combining these tools gives me a powerful launching and navigating experience.</p>

<h3 id="text-expansion">Text Expansion</h3>

<p>In the realm of text expansion, I&rsquo;ve been a longtime user of <strong><a href="https://textexpander.com/">TextExpander</a></strong>, with its snippet search capabilities and reminders making it an indispensable part of my writing process. I&rsquo;ve also started using <strong><a href="https://blaze.today/">TextBlaze</a></strong>, but I&rsquo;ve found myself confusingly using both tools at once. TextExpander&rsquo;s ability to run shell scripts gives it an edge that can’t be ignored, and its expansion after whitespace is something that TextBlaze doesn’t quite match.</p>

<p>My current favorite text expansion tool, however, is <strong><a href="https://cotypist.app/">Cotypist</a></strong>. This innovative tool offers automatic text completion as I type in any application. It predicts my inputs with uncanny accuracy, making it my go-to for prose writing. I don&rsquo;t have to create a ton of snippets, it just figures out what I&rsquo;m typing and fills in the blanks. It works great in apps like Cursor, too, completing instructions and prompts accurately.</p>

<h3 id="window-management">Window Management</h3>

<p>For window management, <strong><a href="https://manytricks.com/moom/">Moom</a></strong> is my weapon of choice. It allows me to move and resize windows with ease, using keyboard shortcuts that fit seamlessly into my workflow. The grid mode, in particular, lets me draw sizes and positions for windows on the fly, making multitasking a breeze and alleviating the need to have a thousand shortcuts for different positioning.</p>

<h3 id="web-browsing">Web Browsing</h3>

<p>In my browsing experience, <strong><a href="https://vimium.github.io/">Vimium</a></strong> has become indispensable. It enhances productivity in my favorite browsers by adding Vim-style navigation to web pages. If I type <span class="keycombo separated" title="F"><kbd class="key symbol">F</kbd></span>, it tags every visible link with key sequences, enabling navigation that feels quick and fluid.</p>

<p>Coupled with my preferred search engine, <strong><a href="https://kagi.com/">Kagi</a></strong>, which provides the ability to navigate search results entirely with the keyboard, my online experience is streamlined. While DuckDuckGo offers similar shortcuts, Kagi&rsquo;s advanced AI slop filtering adds to its appeal, making it a superior choice for my digital searches.</p>

<p>I hope you&rsquo;ve found something new in all of this. I&rsquo;d love to hear about your own favorite keyboard tools and shortcuts: please share them over at the <a href="https://forum.brettterpstra.com">forum</a> and let&rsquo;s master the keyboard!</p>

<p>Here&rsquo;s a combined list of every app mentioned in this post:</p>

<ul>
  <li><a href="https://kindavim.app/">KindaVim</a></li>
  <li><a href="https://superkey.app/">Superkey</a></li>
  <li><a href="https://wooshy.app/">Wooshy</a></li>
  <li><a href="https://scrolla.app/">Scrolla</a></li>
  <li><a href="https://brettterpstra.com/projects/keybindings/">Brett&rsquo;s Keybindings</a></li>
  <li><a href="https://www.keyboardmaestro.com/">Keyboard Maestro</a></li>
  <li><a href="https://manytricks.com/moom/">Moom</a></li>
  <li><a href="https://folivora.ai/">BetterTouchTool</a></li>
  <li><a href="https://karabiner-elements.pqrs.org/">Karabiner-Elements</a></li>
  <li><a href="https://textexpander.com/">TextExpander</a></li>
  <li><a href="https://blaze.today/">TextBlaze</a></li>
  <li><a href="https://kagi.com/">Kagi</a></li>
  <li><a href="https://vimium.github.io/">Vimium</a></li>
  <li><a href="https://cotypist.app/">Cotypist</a></li>
  <li><a href="https://github.com/mikker/LeaderKey">LeaderKey</a></li>
  <li><a href="https://www.obdev.at/launchbar">LaunchBar</a></li>
  <li><a href="https://brettterpstra.com/karabiner/">My Karabiner mods</a></li>
  <li><a href="https://kapeli.com/dash">Dash</a></li>
  <li><a href="https://brettterpstra.com/2026/02/15/niftymenu-tahoe-edition/">NiftyMenu</a></li>
  <li><a href="https://brettterpstra.com/projects/cheaters/">Cheaters</a></li>
  <li><a href="https://appmakes.io/paletro">Paletro</a></li>
</ul>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116301168840727377">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=My+ultimate+keyboard-driven+Mac+utility+list%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F27%2Fmy-ultimate-keyboard-driven-mac-utility-list%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F27%2Fmy-ultimate-keyboard-driven-mac-utility-list%2F&text=My+ultimate+keyboard-driven+Mac+utility+list&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F27%2Fmy-ultimate-keyboard-driven-mac-utility-list%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17307461.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/03/keyboard-header-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/03/keyboard-header-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Apex 0.1.100 - image rendering in terminal]]></title>
    <link href="https://brett.trpstra.net/link/535/17305149/apex-0-dot-1-100-image-rendering-in-terminal"/>
    <updated>2026-03-24T08:00:00-05:00</updated>
    <published>2026-03-24T08:00:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/24/apex-0-dot-1-100-image-rendering-in-terminal</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Apex can <a href="https://brettterpstra.com/2026/02/18/apex-as-terminal-markdown-renderer/">render Markdown to the terminal</a> (<code class="language-plaintext highlighter-rouge">-t terminal</code> or <code class="language-plaintext highlighter-rouge">-t terminal256</code>) with ANSI colors and themes. As of 0.1.100, it can also <em>draw images inline</em> when stdout is a real TTY: your <code class="language-plaintext highlighter-rouge">![alt](path-or-url)</code> images show up as actual graphics instead of only link-style text.</p>

<h3 id="what-actually-draws-the-image">What actually draws the image</h3>

<p>Apex does not embed a rasterizer. It looks for an external viewer on your <code class="language-plaintext highlighter-rouge">PATH</code>, in this order:</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">imgcat</code></strong> (iTerm2-style inline images)</li>
  <li><strong><a href="https://hpjansson.org/chafa/">chafa</a></strong></li>
  <li><strong><a href="https://github.com/atanunq/viu">viu</a></strong></li>
  <li><strong><a href="https://github.com/posva/catimg">catimg</a></strong></li>
</ol>

<p>The first one that exists wins. If none are found, or something fails, or you are piping output (not a TTY), you get the same <strong>link-style</strong> fallback as a normal terminal link: styled alt text plus the URL in parentheses.</p>

<p>Remote <strong><code class="language-plaintext highlighter-rouge">http://</code></strong> and <strong><code class="language-plaintext highlighter-rouge">https://</code></strong> images are downloaded with <strong><code class="language-plaintext highlighter-rouge">curl</code></strong> (temp file under <code class="language-plaintext highlighter-rouge">TMPDIR</code> or <code class="language-plaintext highlighter-rouge">/tmp</code>, then deleted). There is a size cap and timeout so runaway downloads do not blow up your session.</p>

<h3 id="flags-and-metadata">Flags and metadata</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">--no-terminal-images</code></strong> turns inline rendering off entirely (always link-style).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">--terminal-image-width N</code></strong> sets the maximum width in <strong>character cells</strong> (default 50). This is separate from <strong><code class="language-plaintext highlighter-rouge">--width</code></strong>, which wraps prose.</li>
</ul>

<p>You can also set <strong><code class="language-plaintext highlighter-rouge">terminal.inline_images</code></strong> / <strong><code class="language-plaintext highlighter-rouge">terminal_inline_images</code></strong> and <strong><code class="language-plaintext highlighter-rouge">terminal.image_width</code></strong> / <strong><code class="language-plaintext highlighter-rouge">terminal_image_width</code></strong> in metadata or config.</p>

<h3 id="installing-the-viewers-macos">Installing the viewers (macOS)</h3>

<p><strong>iTerm2</strong> ships <strong><code class="language-plaintext highlighter-rouge">imgcat</code></strong> on your <code class="language-plaintext highlighter-rouge">PATH</code> when you use its utilities, so you may already have the first choice. The others are a quick Homebrew install:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Optional: pick one or more (Apex uses the first available on PATH)</span>
brew <span class="nb">install </span>chafa viu catimg</code></pre></div></div>

<p>On Linux, use your distro packages or the projects&rsquo; install notes; the same binary names apply.</p>

<h3 id="other-stuff">Other stuff</h3>

<p>Since <strong>0.1.95</strong>, this line of releases also landed a bunch of other work. Highlights:</p>

<ul>
  <li><strong>CSV/TSV includes</strong> with custom <strong>delimiters</strong> (<code class="language-plaintext highlighter-rouge">{delimiter=X}</code> or <code class="language-plaintext highlighter-rouge">{;}</code>) across iA Writer, Marked, and MultiMarkdown include styles.</li>
  <li><strong>Metadata</strong> handling improved: mode-aware extraction, better MultiMarkdown / Unified / Kramdown behavior, and <strong>standalone HTML</strong> now emits generic metadata as proper <code class="language-plaintext highlighter-rouge">&lt;meta name="..."&gt;</code> tags.</li>
  <li><strong>MultiMarkdown</strong> includes and transclusions accept embedded delimiter overrides without breaking on braces in paths.</li>
  <li><strong>Swift</strong> tooling: <strong><code class="language-plaintext highlighter-rouge">ApexC</code></strong> exposes the C API for SwiftPM, collision fixes for <code class="language-plaintext highlighter-rouge">apex_*</code> symbols, and <strong><code class="language-plaintext highlighter-rouge">NSString.defaultApexOptions()</code></strong> for plugins that need low-level options.</li>
  <li><strong>HTML output shape:</strong> <strong><code class="language-plaintext highlighter-rouge">--to xhtml</code></strong> serializes void elements in XML style (<code class="language-plaintext highlighter-rouge">&lt;br /&gt;</code>, self-closing <code class="language-plaintext highlighter-rouge">meta</code>/<code class="language-plaintext highlighter-rouge">link</code>, and so on). <strong><code class="language-plaintext highlighter-rouge">--to strict-xhtml</code></strong> goes further for full documents: with <strong><code class="language-plaintext highlighter-rouge">--standalone</code></strong> it adds polyglot XHTML scaffolding (XML declaration, XHTML namespace, <code class="language-plaintext highlighter-rouge">application/xhtml+xml</code> metadata). Use one or the other; they target different strictness levels.</li>
  <li><strong>Homebrew</strong> formula updates for recent releases.</li>
</ul>

<p>If you want the full blow-by-blow, see the <a href="https://github.com/ApexMarkdown/apex/blob/main/CHANGELOG.md">project changelog on GitHub</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116284315126521295">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Apex+0.1.100+-+image+rendering+in+terminal%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F24%2Fapex-0-dot-1-100-image-rendering-in-terminal%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F24%2Fapex-0-dot-1-100-image-rendering-in-terminal%2F&text=Apex+0.1.100+-+image+rendering+in+terminal&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F24%2Fapex-0-dot-1-100-image-rendering-in-terminal%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17305149.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Better Keyboard Shortcut Formatting for Writers]]></title>
    <link href="https://brett.trpstra.net/link/535/17305120/better-keyboard-shortcut-formatting-for-writers"/>
    <updated>2026-03-24T07:04:00-05:00</updated>
    <published>2026-03-24T07:04:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/24/better-keyboard-shortcut-formatting-for-writers</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/kbd-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>If you write documentation, tutorials, or notes that include keyboard shortcuts, you know how annoying consistency can be. Sometimes you want plain text like <code class="language-plaintext highlighter-rouge">Shift-Command-K</code>, sometimes you want nice symbolic output like <code class="language-plaintext highlighter-rouge">⇧⌘K</code>, and sometimes you want HTML keycaps.</p>

<p>This project started as a <a href="https://github.com/ttscoff/JekyllPlugins/tree/master/KBDTag">Jekyll plugin</a>, then became a <a href="https://wordpress.org/plugins/bt-keyboard-shortcuts/">WordPress plugin</a>, and now I&rsquo;m offering it as <a href="https://github.com/ttscoff/kbd">a CLI and Automator Actions</a> you can use anywhere.</p>

<figure title="" class="animated_vid_frame" data-caption="&#9654; MP4 (523.5KB)" style="padding-bottom:64.58%" tabindex="0">
              <video class="lazy" muted="" loop="" playsinline="" poster="https://cdn3.brettterpstra.com/uploads/2026/03/KBD.compressed.jpg" style="background:center/contain no-repeat url('https://cdn3.brettterpstra.com/uploads/2026/03/KBD.compressed.jpg')">
                <source src="https://cdn3.brettterpstra.com/uploads/2026/03/KBD.compressed.webm" type="video/webm" />
                <source src="https://cdn3.brettterpstra.com/uploads/2026/03/KBD.compressed.mp4" type="video/mp4" />
              </video>
            </figure>

<h3 id="what-the-scripts-do">What the scripts do</h3>

<p>The project ships two Ruby scripts that parse keyboard shortcut text and normalize it into consistent output, following Apple&rsquo;s guidelines by default:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kbd.rb</code> outputs HTML keycap markup.</li>
  <li><code class="language-plaintext highlighter-rouge">kbd-text.rb</code> outputs plain text shortcuts.</li>
</ul>

<p>Both scripts accept flexible input formats, including:</p>

<ul>
  <li>Keybinding Symbol style: <code class="language-plaintext highlighter-rouge">"$@k"</code></li>
  <li>Text style: <code class="language-plaintext highlighter-rouge">"shift cmd k"</code></li>
  <li>Hyphenated style: <code class="language-plaintext highlighter-rouge">"Shift-Command-k"</code></li>
  <li>Multiple combos separated by <code class="language-plaintext highlighter-rouge">" / "</code></li>
</ul>

<p>For keybinding style:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">Modifier</th>
      <th>Symbol</th>
      <th>Shortcut</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">Shift</td>
      <td><span class="keycombo separated" title="Shift"><kbd class="mod symbol">&#8679;</kbd></span></td>
      <td><code class="language-plaintext highlighter-rouge">$</code></td>
    </tr>
    <tr>
      <td style="text-align: right">Option</td>
      <td><span class="keycombo separated" title="Option"><kbd class="mod symbol">&#8997;</kbd></span></td>
      <td><code class="language-plaintext highlighter-rouge">~</code></td>
    </tr>
    <tr>
      <td style="text-align: right">Command</td>
      <td><span class="keycombo separated" title="Command"><kbd class="mod symbol">&#8984;</kbd></span></td>
      <td><code class="language-plaintext highlighter-rouge">@</code></td>
    </tr>
    <tr>
      <td style="text-align: right">Control</td>
      <td><span class="keycombo separated" title="Control"><kbd class="mod symbol">&#8963;</kbd></span></td>
      <td><code class="language-plaintext highlighter-rouge">^</code></td>
    </tr>
  </tbody>
</table>

<p>In practice, this means you can type shortcuts however you naturally type them, and the tool will clean and format them for you.</p>

<h3 id="download-and-install-as-quick-actions">Download and install as Quick Actions</h3>

<p>Grab the latest release from:</p>

<ul>
  <li><a href="https://github.com/ttscoff/kbd/releases/latest">Latest release download</a></li>
</ul>

<p>Then install:</p>

<ol>
  <li>Download <code class="language-plaintext highlighter-rouge">KBD Automator Actions.zip</code>.</li>
  <li>Unzip it.</li>
  <li>Double-click each <code class="language-plaintext highlighter-rouge">.workflow</code> file.</li>
  <li>Confirm installation when macOS prompts.</li>
</ol>

<p>After that, the actions show up as Services/Quick Actions for text handling.</p>

<h3 id="add-keyboard-shortcuts-for-the-services">Add keyboard shortcuts for the Services</h3>

<p>Once installed, assign your own hotkeys:</p>

<ol>
  <li>Open <strong>System Settings</strong>.</li>
  <li>Go to <strong>Keyboard</strong>.</li>
  <li>Click <strong>Keyboard Shortcuts</strong>.</li>
  <li>Select <strong>Services</strong> on the left.</li>
  <li>Find <strong>Text -&gt; KBD *</strong>.</li>
  <li>Click the shortcut field on the right.</li>
  <li>Press the shortcut you want.</li>
</ol>

<p>That is it. You now have a fast shortcut-to-formatted-shortcut pipeline available from anywhere.</p>

<h3 id="configuration">Configuration</h3>

<p>The scripts read config from:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">~/.config/kbd/config.yaml</code></li>
</ul>

<p>You do not need to create this manually. The first time either script runs, it writes
this file with default values.</p>

<p>Available keys under <code class="language-plaintext highlighter-rouge">kbd:</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">use_modifier_symbols</code> (default: <code class="language-plaintext highlighter-rouge">true</code>)
    <ul>
      <li><code class="language-plaintext highlighter-rouge">true</code>: prefer symbols like <code class="language-plaintext highlighter-rouge">⌘</code>, <code class="language-plaintext highlighter-rouge">⌥</code>, <code class="language-plaintext highlighter-rouge">⇧</code></li>
      <li><code class="language-plaintext highlighter-rouge">false</code>: use words like <code class="language-plaintext highlighter-rouge">Command</code>, <code class="language-plaintext highlighter-rouge">Option</code>, <code class="language-plaintext highlighter-rouge">Shift</code></li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">use_key_symbols</code> (default: <code class="language-plaintext highlighter-rouge">true</code>)
    <ul>
      <li><code class="language-plaintext highlighter-rouge">true</code>: use symbolic key names where possible</li>
      <li><code class="language-plaintext highlighter-rouge">false</code>: use text key names</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">use_plus_sign</code> (default: <code class="language-plaintext highlighter-rouge">false</code>)
    <ul>
      <li><code class="language-plaintext highlighter-rouge">false</code>: combine modifier symbols with no separator (<code class="language-plaintext highlighter-rouge">⇧⌘K</code>)</li>
      <li><code class="language-plaintext highlighter-rouge">true</code>: join with <code class="language-plaintext highlighter-rouge">+</code> (<code class="language-plaintext highlighter-rouge">⇧+⌘+K</code>)</li>
    </ul>
  </li>
</ul>

<h3 id="build-and-release-it-yourself">Build and release it yourself</h3>

<p>If you want to generate or ship your own builds, the repo includes Rake tasks for:</p>

<ul>
  <li>building self-contained scripts</li>
  <li>generating signed Automator workflows</li>
  <li>packaging release zip files</li>
  <li>bumping versions and publishing GitHub releases</li>
</ul>

<p>Project link:</p>

<ul>
  <li><a href="https://github.com/ttscoff/kbd">ttscoff/kbd on GitHub</a></li>
</ul>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116284172396625333">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Better+Keyboard+Shortcut+Formatting+for+Writers%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F24%2Fbetter-keyboard-shortcut-formatting-for-writers%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F24%2Fbetter-keyboard-shortcut-formatting-for-writers%2F&text=Better+Keyboard+Shortcut+Formatting+for+Writers&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F24%2Fbetter-keyboard-shortcut-formatting-for-writers%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17305120.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/03/kbd-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/03/kbd-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Doing updates]]></title>
    <link href="https://brett.trpstra.net/link/535/17304741/doing-updates"/>
    <updated>2026-03-23T15:00:00-05:00</updated>
    <published>2026-03-23T15:00:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/23/doing-updates</id>
    <content type="html"><![CDATA[
<p>Big update for <a href="https://brettterpstra.com/projects/doing"><code class="language-plaintext highlighter-rouge">doing</code></a>: a lot of quality-of-life work since my last post, plus some genuinely useful time-reporting features.</p>

<p>If you want the full docs, start with the <a href="https://github.com/ttscoff/doing/wiki">wiki</a>.</p>

<h3 id="time-budgets">Time Budgets</h3>

<p>You can now set time budgets per tag and see how much you have left as you track work.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="go">doing budget dev 100h
doing budget meetings 10h
doing budget
doing budget dev --remove
</span></code></pre></div></div>

<p>Totals output now shows remaining budget per tag and an overall &ldquo;total budgets left&rdquo; footer when budgets are configured. The <code class="language-plaintext highlighter-rouge">byday</code> export also includes budget info in daily and grand totals.</p>

<h3 id="more-flexible-totals-grouping">More Flexible Totals Grouping</h3>

<p>Totals can now be grouped by tags or sections, and you can repeat grouping flags to control output order.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="go">doing show --totals --by section --by tags
doing show --totals --by tags --by section
</span></code></pre></div></div>

<p>There are also aliases for section grouping (<code class="language-plaintext highlighter-rouge">project</code>, <code class="language-plaintext highlighter-rouge">p</code>), so this works too:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="go">doing show --totals --by project
</span></code></pre></div></div>

<h3 id="new-totals-formats-including-averages">New Totals Formats, Including Averages</h3>

<p>You can now pick the totals time format directly from the command line with <code class="language-plaintext highlighter-rouge">--totals_format</code>.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="go">doing show --totals --totals_format hmclock
doing show --totals --totals_format natural
</span></code></pre></div></div>

<p>There is also a new <code class="language-plaintext highlighter-rouge">averages</code> mode that appends hours/minutes and average hours per day to the total line.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="go">doing show --totals --totals_format averages
</span></code></pre></div></div>

<p>That gives you output in the spirit of:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>Total tracked: 26:03 (26h 3 min, 8.12h/day)</code></pre></div></div>

<p>You can set a default with the <code class="language-plaintext highlighter-rouge">totals_format</code> config key and still override it per command when needed.</p>

<h3 id="better-export-consistency">Better Export Consistency</h3>

<p>Totals grouping now carries through exports more consistently, including HTML, Markdown, JSON, Day One, template, and wiki outputs. JSON totals also gained budget-related fields (<code class="language-plaintext highlighter-rouge">budget</code>, <code class="language-plaintext highlighter-rouge">remaining</code>, <code class="language-plaintext highlighter-rouge">remaining_formatted</code>) for each tag, which makes downstream automation easier.</p>

<p>For details on output and display options, see wiki pages like <a href="https://github.com/ttscoff/doing/wiki/Displaying-Entries">Displaying Entries</a>, <a href="https://github.com/ttscoff/doing/wiki/Time-Tracking">Time Tracking</a>, and <a href="https://github.com/ttscoff/doing/wiki/Configuration">Configuration</a>.</p>

<h3 id="other-stuff">Other Stuff</h3>

<ul>
  <li>Ruby 4 compatibility improved by falling back to <code class="language-plaintext highlighter-rouge">reline</code> when <code class="language-plaintext highlighter-rouge">readline</code> is unavailable.</li>
  <li>Dashed aliases now work for underscore flags and subcommands (<code class="language-plaintext highlighter-rouge">--only-timed</code>, <code class="language-plaintext highlighter-rouge">--tag-sort</code>, <code class="language-plaintext highlighter-rouge">doing tag-dir</code>, etc.).</li>
  <li>Interactive finish handling was fixed for section filters that can resolve to multiple values.</li>
  <li>Time range parsing and normalization got several fixes (<code class="language-plaintext highlighter-rouge">done --from</code>, noon/12pm edge cases, and reset formatting issues).</li>
  <li>Non-interactive runs no longer reopen <code class="language-plaintext highlighter-rouge">/dev/tty</code> for defaults.</li>
  <li>Human totals box formatting and table alignment were cleaned up.</li>
  <li>A few config and test harness rough edges were fixed.</li>
</ul>

<p>As usual, if you run into anything odd, open an issue or PR. This was a nice round of polish plus some features that should make time reporting much more useful day to day.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116280298420114301">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Doing+updates%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F23%2Fdoing-updates%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F23%2Fdoing-updates%2F&text=Doing+updates&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F23%2Fdoing-updates%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17304741.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A Faster Drag-and-Drop Workflow with Dropzone 5 [Sponsor]]]></title>
    <link href="https://brett.trpstra.net/link/535/17304505/a-faster-drag-and-drop-workflow-with-dropzone-5-sponsor"/>
    <updated>2026-03-23T08:00:00-05:00</updated>
    <published>2026-03-23T08:00:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/23/a-faster-drag-and-drop-workflow-with-dropzone-5-sponsor</id>
    <content type="html"><![CDATA[

<p>Thanks to Aptonic and Dropzone for sponsoring BrettTerpstra.com this week! I&rsquo;ve been a Dropzone fan for as long as I can remember, and yes, I use it every day. It&rsquo;s a wonderfully extensible tool that&rsquo;s always available for things like sharing files, processing images, and opening apps. I&rsquo;ve also gotten really used to using it&rsquo;s drop drawer as a way to collect files, and even started incorporating the command line tool for all kinds of automation.</p>

<p><a href="https://aptonic.com/?utm_source=brettterpstra" rel="nofollow"><p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/Dropzone5.jpg"></p></a></p>

<p>Dropzone is a menu bar productivity tool that gives you a faster way to move, copy, and share files, launch apps, and trigger all sorts of time-saving drag-and-drop actions without breaking your flow.</p>

<p><a href="https://aptonic.com/blog/dropzone-5-released?utm_source=brettterpstra">The newly released Dropzone 5</a> is a substantial update and has been redesigned for macOS Tahoe with a cleaner interface, smoother animations and support for Liquid Glass.</p>

<p>The update also adds several workflow-focused improvements. Multiple grids make it easy to separate actions by project or context, deeper grid customization gives you more control over categories, columns, and layout, and folder-based actions can now display the custom icons and colors you&rsquo;ve assigned in Finder, which makes it easier to identify your folders in Dropzone at a glance.</p>

<p>If you like to automate from the command line, Dropzone 5 includes a powerful command line tool for Terminal integration too. You can run actions, manage files in Drop Bar, and switch between grids via the command line tool, making Dropzone 5 a better fit for scripted workflows than ever before.</p>

<p>Dropzone 5 is available as a <a href="https://aptonic.com/?utm_source=brettterpstra" rel="nofollow">free download from Aptonic</a>, with a Pro upgrade available that adds more advanced features.</p>

<p>For a limited time, the Pro upgrade is available at a 30% discount with the coupon code <strong>LAUNCH</strong>.</p>

<p>Visit <a href="https://aptonic.com/?utm_source=brettterpstra" rel="nofollow">Aptonic&rsquo;s website</a> to learn more and download Dropzone 5.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116278812408905823">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=A+Faster+Drag-and-Drop+Workflow+with+Dropzone+5+%5BSponsor%5D%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F23%2Fa-faster-drag-and-drop-workflow-with-dropzone-5-sponsor%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F23%2Fa-faster-drag-and-drop-workflow-with-dropzone-5-sponsor%2F&text=A+Faster+Drag-and-Drop+Workflow+with+Dropzone+5+%5BSponsor%5D&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F23%2Fa-faster-drag-and-drop-workflow-with-dropzone-5-sponsor%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17304505.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/03/Dropzone5.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/03/Dropzone5.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[The Unite Pro giveaway winners!]]></title>
    <link href="https://brett.trpstra.net/link/535/17301626/the-unite-pro-giveaway-winners"/>
    <updated>2026-03-18T12:00:00-05:00</updated>
    <published>2026-03-18T12:00:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/18/the-unite-pro-giveaway-winners</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2023/09/Unite-Pro-winners-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>The Unite Pro giveaway has ended, and I have winners to announce!</p>

<h3 id="the-winners">The winners!</h3>

<p>Congratulations to:</p>

<ul>
  <li>Sven Sackers</li>
  <li>Michael Steadman</li>
  <li>Jake Bordens</li>
  <li>David Banham</li>
</ul>

<p>You should have received an email with details, please <a href="https://brettterpstra.com/contact/">let me know</a> if you didn&rsquo;t hear anything!</p>

<h3 id="but-i-didnt-win">But I didn&rsquo;t win!</h3>

<p>If you didn&rsquo;t win, sorry, but <a href="http://bzgapps.com/unite">Unite Pro is still worth checking out</a>. Turn your favorite websites into apps with Unite Pro! You might not have won, but you can still save 20% at <a href="http://bzgapps.com/unite">bzgapps.com/unite</a> with code <code class="language-plaintext highlighter-rouge">Brett</code>.</p>

<p>By the way, Unite Pro is also available on <a href="https://go.setapp.com/stp44">Setapp</a>, along with hundreds of other amazing apps. You should probably <a href="https://go.setapp.com/stp44">get a subscription</a>.</p>

<p>That&rsquo;s the end of this giveaway series for now!</p>

<p>If you want to suggest an app you&rsquo;d like to see in this series, let me know on <a href="https://brettterpstra.com/contact/">via email</a> or on <a href="https://nojack.easydns.ca/@ttscoff/">Mastodon</a>, and <a href="https://brettterpstra.com/subscribe/">join the email</a> list for notifications!</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116251334888546840">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=The+Unite+Pro+giveaway+winners%21%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F18%2Fthe-unite-pro-giveaway-winners%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F18%2Fthe-unite-pro-giveaway-winners%2F&text=The+Unite+Pro+giveaway+winners%21&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F18%2Fthe-unite-pro-giveaway-winners%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17301626.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2023/09/Unite-Pro-winners-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2023/09/Unite-Pro-winners-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[bt-linkding available on the Chrome and Firefox extension stores]]></title>
    <link href="https://brett.trpstra.net/link/535/17298833/bt-linkding-available-on-the-chrome-and-firefox-extension-stores"/>
    <updated>2026-03-13T08:31:00-05:00</updated>
    <published>2026-03-13T08:31:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/13/bt-linkding-available-on-the-chrome-and-firefox-extension-stores</id>
    <content type="html"><![CDATA[
<p>This is just a quick note to point out that <a href="https://brettterpstra.com/2026/03/04/a-slightly-better-linkding-extension-for-firefox-and-chrome/">my port of the linkding browser extension for Chrome and Firefox</a> is now available on both of the respective extension stores.</p>

<ul>
  <li><strong>Firefox</strong> via <a href="https://addons.mozilla.org/en-US/firefox/addon/bt-linkding/">the Firefox extensions marketplace</a></li>
  <li><strong>Chrome</strong> via <a href="https://chromewebstore.google.com/detail/bt-linkding/kjadapfdpahhhbhlfcgeolgaddkffnpd?pli=1">Chrome Web Store</a></li>
</ul>

<p>Hope you find it useful!</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116222190199285236">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=bt-linkding+available+on+the+Chrome+and+Firefox+extension+stores%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F13%2Fbt-linkding-available-on-the-chrome-and-firefox-extension-stores%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F13%2Fbt-linkding-available-on-the-chrome-and-firefox-extension-stores%2F&text=bt-linkding+available+on+the+Chrome+and+Firefox+extension+stores&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F13%2Fbt-linkding-available-on-the-chrome-and-firefox-extension-stores%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17298833.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for March 11th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17297630/web-excursions-for-march-11th-2026"/>
    <updated>2026-03-11T12:00:00-05:00</updated>
    <published>2026-03-11T12:00:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/11/web-excursions-for-march-11th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with <a href="https://fabric.so/?via=brett">Fabric</a>, the best way to organize your notes, tasks, and projects in one place.</p>

<dl>
  <dt><a href="https://tokie.is/introduction">Tokie</a></dt>
  <dd>Ok, this is cool. A macOS file manager that turns your folder into a database for better file management, with some very cool integration with your AI agent, built-in Markdown editor, custom fields for file management, and a ton of other capabilities.</dd>
  <dt><a href="https://markpub.at/">A proposal for Markdown on ATProto</a></dt>
  <dd>
    <blockquote>
      <p>Providing a Lexicon for putting Markdown in the ATmosphere.</p>
    </blockquote>
  </dd>
  <dd>Fits nicely into my thoughts about a <a href="https://brettterpstra.com/2026/01/02/how-about-a-markdown-web/">Markdown Web</a> and has a fair amount of thought and feedback already in the spec.</dd>
  <dt><a href="https://github.com/10up/actions-wordpress/">GitHub Actions for WordPress</a></dt>
  <dd>I know this has a limited audience, but if you develop WordPress plugins and haven&rsquo;t explored 10up&rsquo;s GitHub actions, you really should. The deploy one is infinitely useful and means you never have to deal with SVN after intial repo setup.</dd>
  <dt><a href="https://github.com/rhsev/matterbase?tab=readme-ov-file">rhsev/matterbase</a></dt>
  <dd>Ralf keeps putting out cool stuff: &ldquo;A database-like TUI for querying frontmatter and YAML in Markdown notes with field filters, full-text search, and table view. For macOS and Linux.&rdquo;</dd>
</dl>

<p>Let Fabric be your second brain, with an all-in-one AI workspace and smart organizer for all your projects, ideas, notes &amp; links. <a href="https://fabric.so/?via=brett">Check it out today.</a></p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116211649871575032">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+March+11th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F11%2Fweb-excursions-for-march-11th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F11%2Fweb-excursions-for-march-11th-2026%2F&text=Web+Excursions+for+March+11th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F11%2Fweb-excursions-for-march-11th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17297630.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Unite Pro giveaway!]]></title>
    <link href="https://brett.trpstra.net/link/535/17296220/unite-pro-giveaway"/>
    <updated>2026-03-09T08:00:00-05:00</updated>
    <published>2026-03-09T08:00:00-05:00</published>
    <id>https://brettterpstra.com//2026/03/09/unite-pro-giveaway</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2023/09/giveaway-Unite-Pro-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;m excited to offer the next giveaway, 4 licenses ($39.99 value each) for <a href="http://bzgapps.com/unite">Unite Pro</a>. Unite has long been my favorite way to create Single Site Browsers (SSBs), sandboxing things like Facebook and MindMeister while adding app-like functionality. The latest version, Unite Pro, is out now, and I have free copies!</p>

<p>From the developer:</p>

<blockquote>
  <p>We&rsquo;ve taken everything we&rsquo;ve learned since 2017 and rethought it for modern macOS. The result is faster, more flexible, and significantly more powerful &mdash; while staying true to what makes Unite valuable: turning web apps into genuine Mac-native experiences.</p>
</blockquote>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/unitepro-sreenshot-800.jpg"></p>

<p>Check out the <a href="http://bzgapps.com/unite">Unite Pro</a> site for more info.</p>

<p>Sign up below to enter. Winners will be randomly drawn on Friday, March 13, at 12pm Central. The drawing is for 4 licenses ($39.99 value each) for Unite Pro, one per winner. Note that if you&rsquo;re reading this via RSS, you&rsquo;ll need to visit <a href="https://brettterpstra.com/2026/03/09/unite-pro-giveaway">this post on brettterpstra.com</a> to enter!</p>

<p>New rule: All signups must have a <strong>first and last name</strong> in order to be eligible. Entries with only a first name will be skipped by the giveaway robot. A lot of the vendors in this series require first and last names for generating license codes, and your cooperation is appreciated!</p>

<p class="sorry"><em>Sorry, this giveaway has ended.</em></p>

<p>If you have an app you&rsquo;d love to see featured in this series of giveaways, <a href="https://brettterpstra.com/contact/">let me know</a>. Also be sure to <a href="https://brettterpstra.com/subscribe/">sign up for the mailing list</a> or <a href="https://hachyderm.io/@ttscoff/">follow me on Mastodon</a> so you can be (among) the first to know about these!</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116199377961039413">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Unite+Pro+giveaway%21%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F09%2Funite-pro-giveaway%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F09%2Funite-pro-giveaway%2F&text=Unite+Pro+giveaway%21&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F09%2Funite-pro-giveaway%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17296220.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2023/09/giveaway-Unite-Pro-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2023/09/giveaway-Unite-Pro-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Put all the web browsers in your Dock]]></title>
    <link href="https://brett.trpstra.net/link/535/17294655/put-all-the-web-browsers-in-your-dock"/>
    <updated>2026-03-06T08:00:00-06:00</updated>
    <published>2026-03-06T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/06/put-all-the-web-browsers-in-your-dock</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/choosy-header-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Here&rsquo;s a silly idea I had. I don&rsquo;t like to keep a bunch of icons in my Dock, preferring to use <a href="https://bunchapp.co">Bunch</a> along with a simple set of persistent Dock apps. I do keep Firefox in the Dock, but I use a bunch of browsers for different purposes, so what if I could access all of them from the Dock without polluting it?</p>

<p>I use <a href="https://choosy.app/" title="Choosy:A smarter default browser for macOS">Choosy</a> as my default browser. It allows me to pop up a menu or select a default browser whenever I open a link, based on custom rules. There are multiple variations of this idea, but Choosy is the original and still my favorite.</p>

<p>Choosy has a <a href="https://choosy.app/api">url scheme</a> that you can use to pop up browser menus, among other things. So getting a menu of all my browsers is as easy as setting up a Shortcut to open <code class="language-plaintext highlighter-rouge">x-choosy://prompt.all/URL</code>. For this purpose, I&rsquo;m using the <a href="https://kagi.com/" title="Kagi Search-A Premium Search Engine">Kagi</a> search page as my URL, since that&rsquo;s where most of my browsing sessions start.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/choosy-shortcut.jpg"></p>

<blockquote class="tip">
  <p>You can also just open <code class="language-plaintext highlighter-rouge">x-choosy://prompt.all/</code> to open a browser without opening a web page.</p>
</blockquote>

<p>Then you can just right click on the Shortcut in the Shortcuts app and &ldquo;Add to Dock,&rdquo; position it where you want it, and you&rsquo;re done. One click to open any of your browsers. I tried adding a custom icon to it, but failed, so I just have a &ldquo;Shortcuts&rdquo; icon in my Dock.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/choosy-browsers-400.jpg"></p>
<p>When I click the shortcut in the Dock, I get the Choosy browser menu, with my running browsers highlighted but all of my browsers accessible.</p>

<p>There&rsquo;s probably a way to get it to work well as a Share item for URLs, but the fact is that if I&rsquo;m opening a URL, I already have Choosy as a share option, and if I&rsquo;m opening from a browser, I have a Choosy plugin to do it. I just want all of my browsers in my Dock without adding 10 icons to it permanently.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/choosy-keyboard-400.jpg"></p>
<p>As a side note, I also set up an Open URL action in <a href="https://github.com/mikker/LeaderKey" title="mikker/LeaderKey:The faster than your launcher launcher">LeaderKey</a> to do the same thing. When you hold down <span class="keycombo separated" title="Command"><kbd class="mod symbol">&#8984;</kbd></span> while the browser picker is open, shortcut overlays appear on the browsers, so you can open any browser with a keyboard shortcut, making launching any browser keyboard-based. Of course, this is somewhat useless if you use a launcher like Alfred or LaunchBar, as your browsers are all a few keystrokes away anyway, so I&rsquo;m just experimenting to see what my brain likes best&hellip;</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116182626715471343">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Put+all+the+web+browsers+in+your+Dock%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F06%2Fput-all-the-web-browsers-in-your-dock%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F06%2Fput-all-the-web-browsers-in-your-dock%2F&text=Put+all+the+web+browsers+in+your+Dock&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F06%2Fput-all-the-web-browsers-in-your-dock%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17294655.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/03/choosy-header-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/03/choosy-header-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for March 5th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17294032/web-excursions-for-march-5th-2026"/>
    <updated>2026-03-05T12:00:00-06:00</updated>
    <published>2026-03-05T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/05/web-excursions-for-march-5th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with Setapp. <a href="https://go.setapp.com/stp44">Get access to hundreds of Mac and iOS apps for one low monthly subscription fee.</a></p>

<dl>
  <dt><a href="https://github.com/julienXX/terminal-notifier">julienXX/terminal-notifier</a></dt>
  <dd>Send User Notifications on macOS from the command-line. A great shell script companion that doesn&rsquo;t require making your own calls to <code class="language-plaintext highlighter-rouge">osascript</code>.</dd>
  <dt><a href="https://github.com/giladdarshan/gdialog">giladdarshan/gdialog: Display macOS dialogs from terminal and scripts</a></dt>
  <dd>Another handy scripting tool to display macOS dialogs from terminal and scripts. There was a more involved version of this that I used to use years ago, but this fits the bill nicely for my current needs.</dd>
  <dt><a href="https://github.com/steveyegge/beads">steveyegge/beads</a></dt>
  <dd>
    <blockquote>
      <p>Beads - A memory upgrade for your coding agent.</p>
    </blockquote>
  </dd>
  <dd>I haven&rsquo;t quite gotten the hang of this yet, but I think it has a lot of potential. A persistent, structured memory for coding agents, replacing markdown plans with a dependency-aware graph to handle long-horizon tasks without losing context.</dd>
  <dt><a href="https://github.com/gsd-build/get-shit-done">Get Sh*t Done</a></dt>
  <dd>
    <blockquote>
      <p>A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code and OpenCode.</p>
    </blockquote>
  </dd>
  <dd>I&rsquo;m not a Claude Code user, but for non-masochistic developers, this looks like an excellent tool.</dd>
</dl>

<p><a href="https://go.setapp.com/stp44">Check out Setapp</a> today and get access to the best Mac and iOS apps out there.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116177908985053238">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+March+5th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F05%2Fweb-excursions-for-march-5th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F05%2Fweb-excursions-for-march-5th-2026%2F&text=Web+Excursions+for+March+5th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F05%2Fweb-excursions-for-march-5th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17294032.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A (slightly better?) linkding extension for Firefox and Chrome]]></title>
    <link href="https://brett.trpstra.net/link/535/17293138/a-slightly-better-linkding-extension-for-firefox-and-chrome"/>
    <updated>2026-03-04T08:00:00-06:00</updated>
    <published>2026-03-04T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/04/a-slightly-better-linkding-extension-for-firefox-and-chrome</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/bt-linkding-header-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve been using <a href="https://github.com/sissbruecker/linkding-extension/">Sascha Ißbrücker&rsquo;s extension</a> for <a href="https://github.com/sissbruecker/linkding">linkding</a> with my <a href="https://bookmarks.scoffb.in">own instance</a> for a while now, and while it&rsquo;s simple and works great, there&rsquo;s one thing that always frustrated me: the popover window closes the moment you click outside of it, erasing what you&rsquo;ve entered.</p>

<p>Obviously, this will only be of use to people who:</p>

<ul>
  <li>Have a linkding server</li>
  <li>Use Firefox or Chrome for general browsing</li>
</ul>

<p>If you fit the target audience, read on!</p>

<p>Picture: you&rsquo;re adding a bookmark, you&rsquo;ve filled in the title and tags, maybe added some notes, and then you realize you need to copy something from the current page. Or maybe you want to paste some text from another window. The moment you click away to grab that text, <em>poof</em> &mdash; the popover closes and you lose everything you&rsquo;ve typed.</p>

<h3 id="the-persistent-popup-solution">The persistent popup solution</h3>

<p>So I created <a href="https://github.com/ttscoff/bt-linkding">bt-linkding</a>, a fork that opens the bookmark panel in a <strong>persistent popup window</strong> instead of the default toolbar popover. The window stays open until you explicitly click <strong>Save</strong>, <strong>Update</strong>, or <strong>Cancel</strong>. No more accidentally dismissing the window and losing your changes.</p>

<p>This means you can freely navigate away from the bookmark panel to copy text from the current page, paste from another window, or do whatever else you need to do. Your work stays put until you&rsquo;re ready to save it.</p>

<p>The extension keeps all the features from the original like tag autocomplete, automatic page description detection, keyboard shortcuts (<code class="language-plaintext highlighter-rouge">Alt+Shift+L</code> to bookmark the current tab), and Omnibox search (type <code class="language-plaintext highlighter-rouge">ld</code> in the address bar). It just fixes that one annoying behavior.</p>

<h3 id="building-and-installing">Building and installing</h3>

<p>Right now, the extension isn&rsquo;t available in the browser stores (though I&rsquo;ve submitted it to both Firefox and Chrome, but this is my first time submitting to either, so no promises about when or if it&rsquo;ll be available there). For now, you&rsquo;ll need to build and install it manually.</p>

<p><strong>Update:</strong> The extension has been approved for Firefox, <a href="https://addons.mozilla.org/en-US/firefox/addon/bt-linkding/">install it from
the store.</a></p>

<p>The process is pretty straightforward. First, clone the repo and install dependencies:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>npm <span class="nb">install</span></code></pre></div></div>

<p>Then build for your target browser. Firefox and Chrome require different background script formats, so you need to use the right build command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># For Firefox</span>
npm run build:firefox

<span class="c"># For Chrome</span>
npm run build:chrome</code></pre></div></div>

<p>This updates the <code class="language-plaintext highlighter-rouge">manifest.json</code> file for the target browser. Once that&rsquo;s done, you can load it as an unpacked extension.</p>

<h3 id="installing-in-firefox">Installing in Firefox</h3>

<p>After running <code class="language-plaintext highlighter-rouge">npm run build:firefox</code>, open Firefox and navigate to <code class="language-plaintext highlighter-rouge">about:debugging</code>. Click &ldquo;This Firefox&rdquo; in the sidebar, then click &ldquo;Load Temporary Add-on&hellip;&rdquo;. Navigate to the project root directory and select any file in the extension (or just the <code class="language-plaintext highlighter-rouge">manifest.json</code> file). The extension will load and you&rsquo;re good to go.</p>

<h3 id="installing-in-chrome">Installing in Chrome</h3>

<p>After running <code class="language-plaintext highlighter-rouge">npm run build:chrome</code>, open Chrome and navigate to <code class="language-plaintext highlighter-rouge">chrome://extensions/</code>. Enable &ldquo;Developer mode&rdquo; (toggle in the top right), then click &ldquo;Load unpacked&rdquo;. Navigate to the project root directory and select it. The extension will load immediately.</p>

<h3 id="whats-next">What&rsquo;s next</h3>

<p>I&rsquo;ve packaged the extension for both stores (<code class="language-plaintext highlighter-rouge">npm run package:firefox</code> and <code class="language-plaintext highlighter-rouge">npm run package:chrome</code> create the zip files), and I&rsquo;ve submitted it to both. But since this is my first time going through the submission process for either store, I&rsquo;m not making any promises about availability or timing. For now, building from source is the way to go.</p>

<p>If the popover closing unexpectedly is a nit for you as well, give this fork a try. The persistent window makes a world of difference when you&rsquo;re trying to add bookmarks with content from multiple sources.</p>

<p>Full instructions and code are <a href="https://github.com/ttscoff/bt-linkding">on GitHub</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116171557755669087">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=A+%28slightly+better%3F%29+linkding+extension+for+Firefox+and+Chrome%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F04%2Fa-slightly-better-linkding-extension-for-firefox-and-chrome%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F04%2Fa-slightly-better-linkding-extension-for-firefox-and-chrome%2F&text=A+%28slightly+better%3F%29+linkding+extension+for+Firefox+and+Chrome&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F04%2Fa-slightly-better-linkding-extension-for-firefox-and-chrome%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17293138.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/03/bt-linkding-header-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/03/bt-linkding-header-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for March 3rd, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17293139/web-excursions-for-march-3rd-2026"/>
    <updated>2026-03-03T12:00:00-06:00</updated>
    <published>2026-03-03T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/03/web-excursions-for-march-3rd-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with Backblaze. <a href="https://secure.backblaze.com/r/00dszk">Back up everything.</a></p>

<dl>
  <dt><a href="https://docs.docmd.io/">docmd &mdash; Minimalist Markdown Docs Generator</a></dt>
  <dd>
    <blockquote>
      <p>Generate beautiful, lightweight static documentation sites directly from your Markdown files with docmd. Zero clutter, just content.</p>
    </blockquote>
  </dd>
  <dd>
    <p>I&rsquo;ve written multiple versions of this concept for myself, ranging from Jekyll plugin-based sites to wholly custom solutions using Apex. This is a very nice, plugin-capable solution that I look forward to trying out for my next documentation project.</p>
  </dd>
  <dt><a href="https://github.com/SourceDocs/SourceDocs">SourceDocs/SourceDocs: Generate Markdown documentation from source code</a></dt>
  <dd>As I get more into Swift development, automatic generation of documentation from source code comments is very nice.</dd>
  <dt><a href="https://github.com/Kapeli/cheatset">Kapeli/cheatset: Generate cheat sheets for Dash</a></dt>
  <dd>I recently had a pull request merged that fixed Cheatset for versions 2.7-4.x compatibility. If you want to make cheat sheets for Dash easily, this provides a DSL for doing so. I&rsquo;ve incorporated it into several workflows, including a new one that uses Apex to convert Markdown documents into Dash cheatsheets that I&rsquo;ll publish soon.</dd>
  <dt><a href="https://www.macbartender.com/Bartender6/blog/">Bartender 6</a></dt>
  <dd>The latest builds of Bartender 6 bring it once again to the top of my list of menu bar managers. Super smooth on Tahoe, and beats Ice and Barbee for me now.</dd>
</dl>

<p>Backblaze securely backs up your entire computer to the cloud, affordably and reliably. I trust it with all my data. <a href="https://secure.backblaze.com/r/00dszk">Check it out today.</a></p>

<p>Like or share this post <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F03%2Fweb-excursions-for-march-3rd-2026%2F&text=Web+Excursions+for+March+3rd%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F03%2Fweb-excursions-for-march-3rd-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17293139.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Generating Man Pages with Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17287905/generating-man-pages-with-apex"/>
    <updated>2026-03-02T08:00:00-06:00</updated>
    <published>2026-03-02T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/02/generating-man-pages-with-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I&rsquo;ve been using a combination of Pandoc and other tools to generate the man page for Apex from Markdown. As Apex nears a 1.0 release, I figured it should be able to generate man pages on its own.</p>

<p>Man pages are what gets shown when you run <code class="language-plaintext highlighter-rouge">man COMMAND</code> in Terminal. They use <code class="language-plaintext highlighter-rouge">roff</code> formatting, which is a pain to write as a human. Writing documentation in Markdown is much easier. Apex can now turn your Markdown into roff man pages or styled HTML man docs. Two output formats cover both classic terminals and the web.</p>

<p><strong><code class="language-plaintext highlighter-rouge">-t man</code></strong> turns Markdown into roff (the traditional man-page source). Use it to generate <code class="language-plaintext highlighter-rouge">apex.1</code>, <code class="language-plaintext highlighter-rouge">my-tool.1</code>, and so on, then install them with <code class="language-plaintext highlighter-rouge">make install</code> or your package manager. No pandoc or go-md2man needed: after <code class="language-plaintext highlighter-rouge">make build</code>, <code class="language-plaintext highlighter-rouge">make man</code> runs the built <code class="language-plaintext highlighter-rouge">apex</code> binary with <code class="language-plaintext highlighter-rouge">-t man</code> to produce the man pages. Write your docs in Markdown and keep a single source for both the website and <code class="language-plaintext highlighter-rouge">man apex</code>.</p>

<p><strong><code class="language-plaintext highlighter-rouge">-t man-html</code></strong> turns the same Markdown into HTML. Without <code class="language-plaintext highlighter-rouge">-s</code> you get a content-only snippet (no wrapper, no nav), handy for embedding. With <code class="language-plaintext highlighter-rouge">-s</code> (standalone) you get a full page: fixed left sidebar TOC (NAME, SYNOPSIS, DESCRIPTION, etc.), a large headline from the NAME section, and optional custom CSS via <code class="language-plaintext highlighter-rouge">--css</code> or <code class="language-plaintext highlighter-rouge">--style</code>. The <code class="language-plaintext highlighter-rouge">document_title</code> metadata (e.g. for <code class="language-plaintext highlighter-rouge">APEX(1)</code>) is used when present. You can also pass <code class="language-plaintext highlighter-rouge">--code-highlight pygments</code> or <code class="language-plaintext highlighter-rouge">--code-highlight skylighting</code> to highlight code blocks in either snippet or standalone output.</p>

<h3 id="see-it-in-action">See it in action</h3>

<p>The Apex man page is available as HTML and as Markdown:</p>

<ul>
  <li><strong>HTML (standalone):</strong> <a href="https://apexmarkdown.org/man/apex.1.html">apex.1.html</a></li>
  <li><strong>Markdown source:</strong> <a href="https://apexmarkdown.org/man/apex.1.md">apex.1.md</a></li>
</ul>

<p>Both are generated from the same Markdown; the HTML is what you get with <code class="language-plaintext highlighter-rouge">apex -t man-html -s</code> (plus any site styling).</p>

<h3 id="what-else-is-new">What else is new</h3>

<p>Highlights from the changelog:</p>

<ul>
  <li><strong>Man page creation:</strong> <code class="language-plaintext highlighter-rouge">-t man</code> and <code class="language-plaintext highlighter-rouge">-t man-html</code> as above; Makefile uses <code class="language-plaintext highlighter-rouge">apex -t man</code> for man targets.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">-t</code> / <code class="language-plaintext highlighter-rouge">--to</code> output formats:</strong> html, json, json-filtered/ast-json/ast, markdown/md, mmd, commonmark/cmark, kramdown, gfm, terminal/cli, terminal256, and now man and man-html.</li>
  <li><strong>Terminal and terminal256 renderers</strong> with theme support, <code class="language-plaintext highlighter-rouge">--theme</code>, and user theme files in <code class="language-plaintext highlighter-rouge">~/.config/apex/terminal/themes/</code>. JSON and AST JSON output before and after filters for tooling.</li>
  <li><strong>Terminal options:</strong> <code class="language-plaintext highlighter-rouge">--width</code> for column wrap; <code class="language-plaintext highlighter-rouge">Terminal.width</code> and <code class="language-plaintext highlighter-rouge">Terminal.theme</code> metadata; theme <code class="language-plaintext highlighter-rouge">list_marker</code> style for bullets and numbers; <code class="language-plaintext highlighter-rouge">Span_classes</code> mapping for inline span classes.</li>
</ul>

<p>For the full list, see <a href="https://github.com/ApexMarkdown/apex/blob/main/CHANGELOG.md">CHANGELOG.md</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116160352356420789">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Generating+Man+Pages+with+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F02%2Fgenerating-man-pages-with-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F02%2Fgenerating-man-pages-with-apex%2F&text=Generating+Man+Pages+with+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F02%2Fgenerating-man-pages-with-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17287905.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for February 27th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17287236/web-excursions-for-february-27th-2026"/>
    <updated>2026-02-27T12:00:00-06:00</updated>
    <published>2026-02-27T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/27/web-excursions-for-february-27th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Whether you&rsquo;re a new user or a seasoned pro, ScreenCastsONLINE offers in-depth screencasts on a wide range of topics, from tutorials to app discovery. <a href="https://screencastsonline.com/members/aff/go/bterpstra">Check it out.</a></p>

<dl>
  <dt><a href="https://dockflow.appitstudio.com/">DockFlow</a></dt>
  <dd>
    <blockquote>
      <p>Boost your productivity with DockFlow. Instantly save, manage, and switch between multiple macOS Docks.</p>
    </blockquote>
  </dd>
  <dd>Interesting app. This is basically how Bunch started (albeit Bunch configuration is text-based). It lets you set up different Dock configurations for different contexts, which I like a lot. It is, in my opinion, overpriced for such a utility, but I also would never begrudge a developer charging what they think their work is worth. Who am I to say?</dd>
  <dt><a href="https://www.producthunt.com/products/openclaw-mac-mini-m4-enclosure">OpenClaw Mac mini M4 Enclosure: Every powerful little crustacean needs a proper shell! | Product Hunt</a></dt>
  <dd>Just for fun.</dd>
  <dd>
    <blockquote>
      <p>The OpenClaw Mac mini M4 Enclosure is a fun, display-worthy 3D printed case for your OpenClaw/Clawdbot/Moltbot device. It’s a perfect blend of cute character + clean desk setup, turning your Mac mini into a chunky little desk companion.</p>
    </blockquote>
  </dd>
  <dt><a href="https://dockey.publicspace.co/">Dockey - Make your Dock faster</a></dt>
  <dd>A simple utility that basically offers GUI access to the animation delay and speed of the macOS Dock hide/show, avoiding the necessity of Terminal commands. Donationware.</dd>
  <dt><a href="https://www.dockfix.app/">DockFix</a></dt>
  <dd>A reasonably-priced Dock enhancer with themes, custom icons, custom widgets, file shelf, and more.</dd>
</dl>

<p>Want more great tips and apps? Check out <a href="https://screencastsonline.com/members/aff/go/bterpstra">ScreenCastsOnline</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116154380405810039">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+February+27th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F27%2Fweb-excursions-for-february-27th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F27%2Fweb-excursions-for-february-27th-2026%2F&text=Web+Excursions+for+February+27th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F27%2Fweb-excursions-for-february-27th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17287236.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A NYT Connections Helper]]></title>
    <link href="https://brett.trpstra.net/link/535/17284282/a-nyt-connections-helper"/>
    <updated>2026-02-26T06:00:00-06:00</updated>
    <published>2026-02-26T06:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/26/a-nyt-connections-helper</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/connections-header-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I love playing the New York Times game Connections. Some days it&rsquo;s easy, occasionally it stumps me. I don&rsquo;t cheat at it &mdash; I&rsquo;ll take a loss when I have to.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/perfect-score-400.jpg"></p>
<p>What I <em>do</em> do, though, is check the built-in hints to find out what order the categories are in. My goal is always to get the purple (hardest) first, which often involves figuring out at least the other three categories first. Once I know all four categories, the guessing game is just which one Connections thinks is hardest.</p>

<div id="paywall_e5758c0b42401f51" class="paywall-container" data-paywall-id="paywall_e5758c0b42401f51" data-content-id="e5758c0b42401f51">
  <div class="paywall-content-placeholder" style="display: none;"></div>
  <div class="paywall-overlay">
    <div class="paywall-message">
      <div class="paywall-icon">🔒</div>
      <h3>Premium Content</h3>
      <p>Join to get the Connections script</p>
      <div class="paywall-actions">
        <a href="https://brettterpstra.com//support/" class="paywall-subscribe-btn">Subscribe Now</a>
        <button class="paywall-login-btn" onclick="paywallLogin()">I'm a Subscriber</button>
      </div>
    </div>
  </div>
</div>

<script>
  // Set up global paywall config and registration queue
  (function() {
    if (!window.paywallConfig) {
      window.paywallConfig = {
        serviceUrl: 'https://paywall.brettterpstra.com',
        cookieName: 'bt_member_token',
        instances: []
      };
    }

    // Register this instance (collected by bt.Paywall when it loads)
    window.paywallConfig.instances.push({
      id: 'paywall_e5758c0b42401f51',
      contentId: 'e5758c0b42401f51'
    });
  })();
</script>

<p>Cheat if you want to.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116137002913823683">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=A+NYT+Connections+Helper%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F26%2Fa-nyt-connections-helper%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F26%2Fa-nyt-connections-helper%2F&text=A+NYT+Connections+Helper&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F26%2Fa-nyt-connections-helper%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17284282.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/connections-header-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/connections-header-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for February 25th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17283540/web-excursions-for-february-25th-2026"/>
    <updated>2026-02-25T06:36:00-06:00</updated>
    <published>2026-02-25T06:36:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/25/web-excursions-for-february-25th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with <a href="https://macpaw.7eer.net/c/228914/82479/1733">CleanMyMac X</a>, all the tools to speed up your Mac, in one app.</p>

<dl>
  <dt><a href="https://github.com/rhsev/grubber">rhsev/grubber</a></dt>
  <dd>Another cool one from Ralf Hülsmann. Turn YAML front matter and YAML code blocks in a bunch of Markdown files into structured data you can search like a database. Like data view without Obsidian.</dd>
  <dt><a href="https://apps.apple.com/us/app/cursor-widget/id6758641320?mt=12">Cursor Widget App</a></dt>
  <dd>A cool (free) idea that lets you connect a macOS desktop widget to Cursor using an MCP. What I&rsquo;ll use it for is yet to be determined, but I like the idea. Right now I&rsquo;m just having it reflect its current todo list to the widget, which is kinda handy, but I think I can do cooler stuff with it.</dd>
  <dt><a href="https://mariozechner.at/posts/2025-11-30-pi-coding-agent/">What I learned building an opinionated and minimal coding agent</a></dt>
  <dd>
    <blockquote>
      <p>Lessons I learned while building my own coding agent from scratch.</p>
    </blockquote>
  </dd>
  <dt><a href="https://github.com/aryankashyap0/shorlabs">aryankashyap0/shorlabs</a></dt>
  <dd>Vercel for Backend. One-click deploy of Python and Node.js apps to AWS with no Docker knowledge needed. Mostly bookmarking this for my own future needs&hellip;</dd>
</dl>

<p><a href="https://brettterpstra.com///macpaw.7eer.net/c/228914/82479/1733"><img src="https://brettterpstra.com///a.impactradius-go.com/display-ad/1733-82479" border="0" alt="CleanMyMac X" width="728" height="90" /></a><img height="0" width="0" src="https://brettterpstra.com///macpaw.7eer.net/i/228914/82479/1733" style="position:absolute;visibility:hidden;" border="0" /></p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116131509887714990">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+February+25th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F25%2Fweb-excursions-for-february-25th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F25%2Fweb-excursions-for-february-25th-2026%2F&text=Web+Excursions+for+February+25th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F25%2Fweb-excursions-for-february-25th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17283540.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for February 20th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17280025/web-excursions-for-february-20th-2026"/>
    <updated>2026-02-20T05:58:00-06:00</updated>
    <published>2026-02-20T05:58:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/20/web-excursions-for-february-20th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with Backblaze. <a href="https://secure.backblaze.com/r/00dszk">Back up everything.</a></p>

<dl>
  <dt><a href="https://devly.techfixpro.net/">Devly — Developer Utilities</a></dt>
  <dd>The fastest way to encode, decode, format, hash, and convert — 50+ developer tools, one click from your menu bar. No subscriptions, no internet required. A nice companion to <a href="https://retina.studio/textbuddy/" title="TextBuddy">TextBuddy</a>⏎.</dd>
  <dt><a href="https://gwfh.mranftl.com/fonts">google webfonts helper</a></dt>
  <dd>Found this nifty tool for downloading very streamlined, compressed versions of Google fonts for self-hosting. Get eot, ttf, svg, woff and woff2 files + CSS snippets.</dd>
  <dt><a href="https://giscus.app/">giscus</a></dt>
  <dd>A comments widget built on GitHub Discussions. If I give up on running comments from <a href="https://forum.brettterpstra.com">my forum Discourse server</a> at some point, this is what I&rsquo;ll switch to.</dd>
  <dt><a href="https://gist.github.com/ttscoff/bbf5a04b25c5dd04d9658e728da26cd7">Cursor iThoughts integration</a></dt>
  <dd>One of my own design: teach Cursor to read iThoughts X mind maps and create implementation plans for them. Just place in .cursor/commands for the project and run <code class="language-plaintext highlighter-rouge">/ithoughts path/to/brainstorm.itmz</code>.</dd>
</dl>

<p>Backblaze securely backs up your entire computer to the cloud, affordably and reliably. I trust it with all my data. <a href="https://secure.backblaze.com/r/00dszk">Check it out today.</a></p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116102915676838157">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+February+20th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F20%2Fweb-excursions-for-february-20th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F20%2Fweb-excursions-for-february-20th-2026%2F&text=Web+Excursions+for+February+20th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F20%2Fweb-excursions-for-february-20th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17280025.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Apex and Multiple Image Formats]]></title>
    <link href="https://brett.trpstra.net/link/535/17278639/apex-and-multiple-image-formats"/>
    <updated>2026-02-18T10:05:00-06:00</updated>
    <published>2026-02-18T10:05:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/18/apex-and-multiple-image-formats</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Here&rsquo;s part two of my Apex posts for the day: Multiple image formats from a single image syntax.</p>

<p>Modern browsers support a bunch of image formats, like WebP and AVIF, that are smaller and sharper than good old PNG and JPEG. The trick is serving them without breaking older browsers. Apex makes that easy with a few attributes on your image syntax.</p>

<p>Apex isn&rsquo;t going to generate the additional images themselves, of course, that&rsquo;s up to you. But if you have a series of images in the same directory, inserting them properly is super simple now. Take a directory with:</p>

<ul>
  <li>image.png</li>
  <li>image@2x.png</li>
  <li>image.webp</li>
  <li>image@2x.webp</li>
  <li>image.avif</li>
  <li>image@2x.avif</li>
</ul>

<p>Now you can generate a <code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> (and optionally a <code class="language-plaintext highlighter-rouge">&lt;figure&gt;</code> with captions) with a single line of Markdown.</p>

<h3 id="webp-and-avif">WebP and AVIF</h3>

<p>Add <code class="language-plaintext highlighter-rouge">webp</code> or <code class="language-plaintext highlighter-rouge">avif</code> after the URL and Apex wraps the image in a <code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> element with the right <code class="language-plaintext highlighter-rouge">&lt;source&gt;</code> tags. Browsers that support the format use it; everyone else falls back to the main image.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Hero image</span><span class="p">](</span><span class="sx">img/hero.png</span> webp)
<span class="p">![</span><span class="nv">Hero AVIF</span><span class="p">](</span><span class="sx">img/hero.png</span> avif)</code></pre></div></div>

<p>You can combine both for maximum compatibility:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Modern formats</span><span class="p">](</span><span class="sx">img/banner.jpg</span> webp avif)</code></pre></div></div>

<p>Both attributes work with <code class="language-plaintext highlighter-rouge">@2x</code> for retina. So <code class="language-plaintext highlighter-rouge">![Retina](img/hero.png webp @2x)</code> produces a srcset with <code class="language-plaintext highlighter-rouge">img.webp</code> at 1x and <code class="language-plaintext highlighter-rouge">img@2x.webp</code> at 2x. Same deal for avif. Apex also recognizes @3x, if you need that.</p>

<h3 id="auto-discovery">Auto discovery</h3>

<p>If you&rsquo;d rather not list every format by hand, use the <code class="language-plaintext highlighter-rouge">auto</code> attribute:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Profile menu</span><span class="p">](</span><span class="sx">img/app-pass-1-profile-menu.jpg</span> auto)</code></pre></div></div>

<p>When <code class="language-plaintext highlighter-rouge">base_directory</code> is set (e.g. from the document path or <code class="language-plaintext highlighter-rouge">--base-directory</code>), Apex checks the filesystem for existing variants. For images it looks for 2x, 3x, webp, and avif. It only emits <code class="language-plaintext highlighter-rouge">&lt;source&gt;</code> elements for files that actually exist, so you can add formats over time without touching the Markdown.</p>

<p>You can also use <code class="language-plaintext highlighter-rouge">*</code> as the extension, which does the same thing:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Profile menu</span><span class="p">](</span><span class="sx">img/app-pass-1-profile-menu.*</span><span class="p">)</span></code></pre></div></div>

<p>That scans for jpg, png, gif, webp, and avif (plus 2x and 3x variants) for images, and mp4, webm, ogg, mov, and m4v for videos. <code class="language-plaintext highlighter-rouge">![](image.*)</code> is equivalent to <code class="language-plaintext highlighter-rouge">![](image.png auto)</code>.</p>

<h3 id="video-same-syntax-different-element">Video: same syntax, different element</h3>

<p>Video URLs in image syntax get special treatment. If the URL ends in mp4, mov, webm, ogg, ogv, or m4v, Apex emits a <code class="language-plaintext highlighter-rouge">&lt;video&gt;</code> tag instead of <code class="language-plaintext highlighter-rouge">&lt;img&gt;</code>:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Demo video</span><span class="p">](</span><span class="sx">media/demo.mp4</span><span class="p">)</span></code></pre></div></div>

<p>That becomes <code class="language-plaintext highlighter-rouge">&lt;video&gt;&lt;source src="https://brettterpstra.commedia/demo.mp4" type="video/mp4"&gt;&lt;/video&gt;</code>. No extra syntax needed.</p>

<p>To add alternative formats for broader browser support, tack on the format name:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Demo with WebM</span><span class="p">](</span><span class="sx">media/demo.mp4</span> webm)
<span class="p">![</span><span class="nv">Demo with OGG</span><span class="p">](</span><span class="sx">media/intro.mp4</span> ogg)</code></pre></div></div>

<p>Apex adds <code class="language-plaintext highlighter-rouge">&lt;source&gt;</code> elements for each format you specify. The primary URL (e.g. <code class="language-plaintext highlighter-rouge">demo.mp4</code>) stays as the fallback; the attributes add webm, ogg, or whatever before it. So <code class="language-plaintext highlighter-rouge">![](video.mp4 ogg)</code> gives you an ogg source plus the mp4 fallback.</p>

<p>Like I said, using <code class="language-plaintext highlighter-rouge">auto</code> or <code class="language-plaintext highlighter-rouge">demo.*</code> as a video URL will generate markup for all the existing formats.</p>

<h3 id="what-this-means">What This Means</h3>

<p>It means proper, accessible markup, and optimized web pages. It won&rsquo;t mean much if your output goal is PDF or DOCX, but it&rsquo;s great for web production.</p>

<p>The <code class="language-plaintext highlighter-rouge">auto</code> and `*` syntaxes mean you can write the Markdown once, and then every time you add a new format or resolution, the correct markup will be generated the next time you render, without touching the Markdown.</p>

<h3 id="quick-reference">Quick reference</h3>

<table>
  <thead>
    <tr>
      <th>Syntax</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url webp)</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> with WebP source</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url avif)</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> with AVIF source</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url webp @2x)</code></td>
      <td>WebP with retina srcset</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url auto)</code></td>
      <td>Discovers formats from disk (requires base_directory)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url.*)</code></td>
      <td>Same as auto—scans jpg/png/gif/webp/avif and video formats</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](video.mp4)</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;video&gt;</code> with mp4 source</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](video.mp4 webm)</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;video&gt;</code> with webm + mp4 fallback</td>
    </tr>
  </tbody>
</table>

<p>Same Markdown image syntax, better output. Check out the <a href="https://brettterpstra.com//projects/apex">Apex wiki</a> for more details.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116092579424853852">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Apex+and+Multiple+Image+Formats%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-and-multiple-image-formats%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-and-multiple-image-formats%2F&text=Apex+and+Multiple+Image+Formats&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-and-multiple-image-formats%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17278639.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Apex as Terminal Markdown Renderer]]></title>
    <link href="https://brett.trpstra.net/link/535/17278640/apex-as-terminal-markdown-renderer"/>
    <updated>2026-02-18T10:00:00-06:00</updated>
    <published>2026-02-18T10:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/18/apex-as-terminal-markdown-renderer</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>This is the first of two <a href="https://brettterpstra.com//projects/apex">Apex</a> posts today. In my opinion, the two things I&rsquo;ve been working on deserve their own headlines. So here&rsquo;s part one: terminal rendering.</p>

<p>If you&rsquo;ve ever wanted to read Markdown in the terminal with syntax highlighting and nice formatting, you&rsquo;ve probably reached for <strong>mdless</strong> or <strong>glow</strong>. They&rsquo;re great tools. But what if you want all of that <em>plus</em> Apex&rsquo;s extensions, filters, and plugins—tables with captions, footnotes, callouts, file includes, and whatever your custom filters do?</p>

<p>That&rsquo;s where Apex&rsquo;s new built-in terminal output comes in.</p>

<h2 id="triggering-terminal-output">Triggering Terminal Output</h2>

<p>Apex can now render Markdown directly to your terminal instead of HTML. You use <code class="language-plaintext highlighter-rouge">--to</code> flag with a special target:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex README.md <span class="nt">--to</span> terminal</code></pre></div></div>

<p>or using the short form:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex README.md <span class="nt">-t</span> cli</code></pre></div></div>

<p>Both produce colored, formatted output suitable for reading in an interactive terminal. No piping to mdless required—Apex handles it natively.
For terminals that support 256 colors, use:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex README.md <span class="nt">-t</span> terminal256</code></pre></div></div>

<p>This gives you a richer color palette for headings, code blocks, links, and other elements. If your terminal supports it, the difference is noticeable.</p>

<h2 id="what-you-get">What You Get</h2>

<p>Because this is Apex doing the rendering, you get the full pipeline:</p>

<ul>
  <li><strong>Extensions</strong>: Tables, footnotes, definition lists, task lists, callouts, wiki links, and all the rest</li>
  <li><strong>Filters</strong>: Any filters in your config run before render, so your title filter, delink filter, or custom Lua scripts all apply</li>
  <li><strong>Plugins</strong>: Pre-parse and post-render plugins run as usual &mdash; kbd for <code class="language-plaintext highlighter-rouge">&lt;kbd&gt;</code> tags, md-fixup, or whatever you&rsquo;ve installed</li>
</ul>

<p>So when you <code class="language-plaintext highlighter-rouge">apex doc.md -t terminal</code>, you&rsquo;re seeing the same processed document you&rsquo;d get as HTML, just rendered for the terminal instead.</p>

<p>It&rsquo;s not perfect for handling all elements yet, but the foundation is there and I&rsquo;ll tweak it as needed.</p>

<h2 id="theming-mdless-compatibility">Theming: mdless Compatibility</h2>

<p>Apex&rsquo;s terminal theming is compatible with <a href="https://brettterpstra.com//projects/mdless">mdless</a> theme files. If you already use mdless, you can point Apex at your existing theme:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex doc.md <span class="nt">-t</span> terminal256 <span class="nt">--theme</span> ~/.config/mdless/mdless.theme</code></pre></div></div>

<p>Or set a default in your Apex config. The theme format matches mdless, so your <code class="language-plaintext highlighter-rouge">~/.config/mdless/mdless.theme</code> (or any <code class="language-plaintext highlighter-rouge">*.theme</code> file) works out of the box.</p>

<p>Apex adds a few extra keys for elements mdless doesn&rsquo;t have—callouts, definition lists, table captions, and the like. If you don&rsquo;t define them, sensible defaults are used. You can override them in your theme file when you want finer control.</p>

<h2 id="json-output-build-your-own-renderer">JSON Output: Build Your Own Renderer</h2>

<p>Apex can also output a Pandoc-compatible JSON representation of the document:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex doc.md <span class="nt">--to</span> json</code></pre></div></div>

<p>This emits the full AST, including blocks, inlines, metadata, to stdout. From there you can pipe it into your own script to generate whatever you want: custom terminal formatting, a different HTML structure, plain text, or something else entirely.</p>

<p>The JSON format is the same one filters use. So if you&rsquo;ve written a filter, you already know the structure. A simple Python script can read it, walk the tree, and emit your own output format. No need to re-implement the parser.</p>

<h2 id="quick-reference">Quick Reference</h2>

<table>
  <thead>
    <tr>
      <th>Option</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-t cli</code> or <code class="language-plaintext highlighter-rouge">-t terminal</code></td>
      <td>Terminal output with basic ANSI colors</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-t terminal256</code></td>
      <td>Terminal output with 256-color palette</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-t json</code></td>
      <td>Raw Pandoc JSON AST for custom processing</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--theme FILE</code></td>
      <td>Use mdless-compatible theme file</td>
    </tr>
  </tbody>
</table>

<p>So next time you&rsquo;re browsing docs in the terminal, skip the Apex &amp;rarr mdless/glow pipeline and let <a href="https://brettterpstra.com//projects/apex">Apex</a> do it all in one shot.</p>

<p>Like or share this post <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-as-terminal-markdown-renderer%2F&text=Apex+as+Terminal+Markdown+Renderer&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-as-terminal-markdown-renderer%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17278640.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[NiftyMenu Tahoe Edition]]></title>
    <link href="https://brett.trpstra.net/link/535/17276851/niftymenu-tahoe-edition"/>
    <updated>2026-02-15T12:57:00-06:00</updated>
    <published>2026-02-15T12:57:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/15/niftymenu-tahoe-edition</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/niftymenutahoe-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p><a href="https://brettterpstra.com/projects/niftymenu">NiftyMenu</a> is the little tool I use to dump a macOS app&rsquo;s menu bar into an HTML page so I can search it, click through to items, and add callouts for screencasts and product documentation. The latest round of updates brings the UI in line with macOS Tahoe and cleans up a bunch of behavior and script logic.</p>

<figure class="bt-video-container" style="padding-bottom:56.25%"><a class="vimeo" href="https://player.vimeo.com/video/1165172881" data-videoid="1165172881" data-width="640" data-height="360">Vimeo Video</a></figure>

<p>I wrote NiftyMenu <a href="https://brettterpstra.com/2019/06/12/niftymenu-mac-menu-madness/">years ago</a>, and did a major update <a href="https://brettterpstra.com/2023/05/04/niftymenu-update-for-ventura/">for Ventura</a>. But it was way behind on Liquid Glass and basically wasn&rsquo;t working anymore.</p>

<h3 id="what-it-is">What it is</h3>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/shortcut-callout.jpg"></p>
<p>Since it&rsquo;s been a while, let me explain NiftyMenu a bit. It&rsquo;s a tool specifically for people documenting or blogging about apps (though anybody might get a kick out of it). When you want a screenshot of a menu item, highlighted (maybe with callouts), getting a menu to stick in place and maintain highlight state while you take a screenshot can be tricky, and doing a bunch of screenshots like this can be a pain.</p>

<p>NiftyMenu creates an HTML version of any app&rsquo;s menu bar. You can click any item to lock it in place, navigate submenus, select items, highlight them, add callouts, and focus the shortcut key. It has dark and light modes, quick search for finding menu items with just the keyboard, customizable desktop images (including random images), and more.</p>

<p>I recommend viewing the pages it produces in Chrome. The styling should work well in any browser, but with Chrome you can just hit <span class="keycombo separated" title="Shift-S"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">S</kbd></span> to save a screenshot of the currently-focused menu item. It&rsquo;s super handy. Screenshotting works in Firefox, but the text gets screwy. And it doesn&rsquo;t work at all in Safari. So use Chrome/Chromium.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/settings-menu.jpg"></p>
<p>At the bottom right of a generated menu page there&rsquo;s a settings box that appears on hover, and you can change things like light/dark mode, background image, callout type, etc.</p>

<p>NiftyMenu is a Ruby script that generates a web app of sorts. Just one page with all the markup necessary for the menu, and some CSS and JavaScript to enable all of its functionality.</p>

<h3 id="styling-to-match-tahoe">Styling to match Tahoe</h3>

<p>The new layout and colors are tuned to feel like Tahoe&rsquo;s menu bar. The font stack uses SF Pro when available (Tahoe and recent macOS), with sensible fallbacks. Top-level menu items get a soft white overlay on hover with rounded corners instead of a flat accent block; the solid accent is reserved for the selected item (<code class="language-plaintext highlighter-rouge">.last</code>). Submenus use the same idea: light grey overlay on hover, accent only on the selected row. Toolbar height, padding, and submenu positioning are adjusted so the overall proportions match the system menus.</p>

<p>Dark mode gets the same treatment: overlay-based hover states and accent only on the selected item. Submenu and divider colors use the new variables so they stay consistent with the rest of the theme.</p>

<h3 id="backgrounds-and-overlays">Backgrounds and overlays</h3>

<p>Unsplash deprecated their simpler API for image embeds, so I switched the random wallpaper generator to use <a href="https://picsum.photos/">Picsum.photos</a>. You can&rsquo;t do a search, but you can get a random image and apply blur and grayscale to it (NiftyMenu gives you the options when getting a random wallpaper). You can also add a &ldquo;seed&rdquo; word, which will stick the image so that it can be retrieved reliably. The seed you enter is stored in browser storage and will automatically be used next time the dialog is opened.</p>

<p>The page background (gradient or desktop image) is now fixed to the viewport, so when the menu list is taller than the window you scroll the content, not the background.</p>

<p>If you use a desktop image, there&rsquo;s a brightening overlay: a semi-transparent layer with <code class="language-plaintext highlighter-rouge">background-blend-mode: overlay</code> so the image reads a bit lighter. You can tweak it (or turn it off) via <code class="language-plaintext highlighter-rouge">$desktop-image-overlay</code> and <code class="language-plaintext highlighter-rouge">$dark-desktop-image-overlay</code> in your variables. The toolbar strip at the top uses the same overlay color and blend so it sits nicely over the gradient or image.</p>

<p>In light mode, when submenus overlap each other or the parent, the overlap darkens instead of lightening. Submenu <code class="language-plaintext highlighter-rouge">ul</code>s use <code class="language-plaintext highlighter-rouge">mix-blend-mode: multiply</code> and an opaque background so you get a clear light bluish-grey panel that darkens where it stacks.</p>

<h3 id="a-few-fixes-while-i-was-at-it">A few fixes while I was at it</h3>

<p>Divider rows no longer highlight on hover or accept clicks. They use <code class="language-plaintext highlighter-rouge">pointer-events: none</code> and the click handler bails out for divider targets and list items that only contain a divider.</p>

<p>The script also fixes the case where a bogus shortcut was attached to a divider line and produced blockquotes in the markdown; that combo is now normalized to a plain divider span before processing.</p>

<p>The globe key in shortcuts is no longer an emoji. The script outputs the entity, then swaps it for a <code class="language-plaintext highlighter-rouge">&lt;span class="globe-icon"&gt;</code> so CSS can draw the icon with a mask and <code class="language-plaintext highlighter-rouge">currentColor</code>, including when the shortcut is in the callout (inverted) state.</p>

<p>There&rsquo;s a bit more progress reporting now, going to STDERR in Terminal while processing, with a simple bar and step labels: gather, process, convert, write.</p>

<h3 id="whats-missing">What&rsquo;s missing</h3>

<p>In macOS, there are often menu items that change to an alternate option when you hold down the <span class="keycombo separated" title="Option"><kbd class="mod symbol">&#8997;</kbd></span> key. With the way NiftyMenu gathers items, these all appear in the same menu, not hidden behind a modifier. I spent an hour this morning trying to get that to work, but it doesn&rsquo;t seem feasible.</p>

<p>I also struggled with overlapping transparencies. In Tahoe, when a submenu&rsquo;s left edge overlaps the parent menu, the edge appears opaque, or at least at uniform transparency with the submenu. Because the HTML version uses CSS colors with alpha channels, the overlap instead becomes a brighter version of the base color. I played with blend modes and other tricks but couldn&rsquo;t find a way to replicate this. It&rsquo;s a very small issue, but one that bugged me. If anyone has suggestions, I&rsquo;d love to hear them.</p>

<p>There&rsquo;s a <a href="https://ttscoff.github.io/niftymenu/jsapi/index.html">JavaScript API</a> for automating the process of selecting menu items using fuzzy search and even saving the screenshots, so you can create a workflow that generates new screenshots even as menu item names and positions change. It&rsquo;s not a perfect solution &mdash; being able to do it with AppleScript would be better, but not an option here.</p>

<p>I also could have, but didn&rsquo;t, set it up so you could choose the OS styling to use. It&rsquo;s always going to use the latest macOS (that it&rsquo;s been updated for) and not be backward compatible. This seems like a reasonable approach to me.</p>

<h3 id="give-it-a-shot">Give it a shot</h3>

<p>If you&rsquo;re writing documentation or are a blogger who often talks about apps&rsquo; menu items and shortcuts, give <a href="https://brettterpstra.com/projects/niftymenu">NiftyMenu</a> a shot. If nothing else, it&rsquo;s a pretty cool proof of concept.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116076286219123700">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=NiftyMenu+Tahoe+Edition%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F15%2Fniftymenu-tahoe-edition%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F15%2Fniftymenu-tahoe-edition%2F&text=NiftyMenu+Tahoe+Edition&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F15%2Fniftymenu-tahoe-edition%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17276851.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/niftymenutahoe-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/niftymenutahoe-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Pandoc-style filters for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17272052/pandoc-style-filters-for-apex"/>
    <updated>2026-02-07T09:36:00-06:00</updated>
    <published>2026-02-07T09:36:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/07/pandoc-style-filters-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>In my quest to make <a href="https://github.com/ApexMarkdown/apex/">Apex</a> as complete as possible before I integrate it into Marked, I&rsquo;ve added Pandoc-compatible filters, which can be written in Lua for native execution, or in any language using a Pandoc JSON pipeline.</p>

<p>The filter system runs your code on the document <em>after</em> parsing and <em>before</em> rendering. The pipeline is: Markdown → AST → <strong>Pandoc-style JSON</strong> → your filters → JSON → HTML. That means you can transform the document in any language that speaks JSON &mdash; Ruby, Python, Lua, Node, whatever &mdash; using the same <a href="https://pandoc.org/filters.html">Pandoc JSON AST</a> that Pandoc uses. If you&rsquo;ve written a Pandoc filter, the proecess is the same: read one JSON document from stdin, write one JSON document to stdout.</p>

<h3 id="running-filters-from-the-cli">Running filters from the CLI</h3>

<p>Three main options:</p>

<ul>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--filter NAME</code></strong> — Run a single filter. Apex looks for an executable named <code class="language-plaintext highlighter-rouge">NAME</code> in your user filters directory (<code class="language-plaintext highlighter-rouge">~/.config/apex/filters</code> or <code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/apex/filters</code>). So <code class="language-plaintext highlighter-rouge">apex --filter title input.md</code> runs <code class="language-plaintext highlighter-rouge">~/.config/apex/filters/title</code>. These can exist inside of subdirectories.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--filters</code></strong> — Run <em>all</em> executables in that directory, in sorted order. Handy if you keep a fixed pipeline (e.g. <code class="language-plaintext highlighter-rouge">01-title</code>, <code class="language-plaintext highlighter-rouge">10-delink</code>) and just want one flag.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--lua-filter FILE</code></strong> — Run a Lua script as a filter. Apex calls <code class="language-plaintext highlighter-rouge">lua FILE</code>; the script reads Pandoc JSON from stdin and writes JSON to stdout. You need a JSON library (e.g. <strong>dkjson</strong>: <code class="language-plaintext highlighter-rouge">luarocks install dkjson</code>). No Pandoc Lua runtime required.</p>
  </li>
</ul>

<p>Filters run in sequence. If any filter exits non-zero or outputs invalid JSON, Apex aborts unless you pass <code class="language-plaintext highlighter-rouge">--no-strict-filters</code> (then it skips the bad filter and continues).</p>

<h3 id="the-central-filter-list-install-and-list">The central filter list: install and list</h3>

<p>There&rsquo;s a small index of filters at <a href="https://github.com/ApexMarkdown/apex-filters">ApexMarkdown/apex-filters</a>. It&rsquo;s a single JSON file listing filter id, title, description, author, repo, etc. This will grow as I and others create new filters.</p>

<ul>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">apex --list-filters</code></strong> — Prints &ldquo;Installed Filters&rdquo; (what&rsquo;s in your <code class="language-plaintext highlighter-rouge">~/.config/apex/filters</code> dir) and &ldquo;Available Filters&rdquo; from that index, with titles and descriptions.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">apex --install-filter ID</code></strong> — Installs a filter by id from the index (e.g. <code class="language-plaintext highlighter-rouge">apex --install-filter unwrap</code>). It clones the repo into your filters directory. You can also install by Git URL or GitHub shorthand (user/repo).</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">apex --uninstall-filter ID</code></strong> — Removes that filter (with a confirmation prompt).</p>
  </li>
</ul>

<p>To add your own filter to the list, open a pull request on <a href="https://github.com/ApexMarkdown/apex-filters">github.com/ApexMarkdown/apex-filters</a>. Once it&rsquo;s merged, everyone can <code class="language-plaintext highlighter-rouge">--list-filters</code> and <code class="language-plaintext highlighter-rouge">--install-filter your-id</code>. <a href="https://github.com/ApexMarkdown/apex-filters?tab=readme-ov-file#how-to-request-adding-a-filter">See the docs</a> for more info on contributing.</p>

<h3 id="example-filters-in-the-wild">Example filters in the wild</h3>

<p>Two good references:</p>

<ul>
  <li>
    <p><strong><a href="https://github.com/ApexMarkdown/apex-filter-uppercase">ApexMarkdown/apex-filter-uppercase</a></strong> — Lua filter that uppercases every <code class="language-plaintext highlighter-rouge">Str</code> inline. Shows how to walk the AST with dkjson and mutate in place.</p>
  </li>
  <li>
    <p><strong><a href="https://github.com/ApexMarkdown/apex-filter-unwrap">ApexMarkdown/apex-filter-unwrap</a></strong> — Lua filter that unwraps elements starting with <code class="language-plaintext highlighter-rouge">&lt; </code> (e.g. custom block markers). More involved AST walking.</p>
  </li>
</ul>

<p>Install them with <code class="language-plaintext highlighter-rouge">apex --install-filter uppercase</code> and <code class="language-plaintext highlighter-rouge">apex --install-filter unwrap</code>, then use <code class="language-plaintext highlighter-rouge">--filter uppercase</code> or <code class="language-plaintext highlighter-rouge">--filter unwrap</code> in your pipeline.</p>

<h3 id="short-ruby-example">Short Ruby example</h3>

<p>A minimal filter that reads Pandoc JSON, does one thing (e.g. prepend an H1 from metadata), and writes JSON back. Ruby&rsquo;s stdlib <code class="language-plaintext highlighter-rouge">json</code> is enough.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c1">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s2">"json"</span>

<span class="n">doc</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="vg">$stdin</span><span class="p">.</span><span class="nf">read</span><span class="p">)</span>
<span class="n">blocks</span> <span class="o">=</span> <span class="n">doc</span><span class="p">[</span><span class="s2">"blocks"</span><span class="p">]</span> <span class="o">||</span> <span class="p">[]</span>
<span class="n">meta</span>   <span class="o">=</span> <span class="n">doc</span><span class="p">[</span><span class="s2">"meta"</span><span class="p">]</span> <span class="o">||</span> <span class="p">{}</span>

<span class="c1"># Get title from meta if present (simplified)</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">meta</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="s2">"title"</span><span class="p">,</span> <span class="s2">"c"</span><span class="p">)</span> <span class="o">||</span> <span class="s2">"Untitled"</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">title</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">String</span><span class="p">)</span> <span class="p">?</span> <span class="n">title</span> <span class="p">:</span> <span class="n">title</span><span class="p">.</span><span class="nf">to_s</span>

<span class="n">header</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s2">"t"</span> <span class="o">=&gt;</span> <span class="s2">"Header"</span><span class="p">,</span>
  <span class="s2">"c"</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="p">[</span><span class="s2">""</span><span class="p">,</span> <span class="p">[],</span> <span class="p">[]],</span> <span class="p">[{</span> <span class="s2">"t"</span> <span class="o">=&gt;</span> <span class="s2">"Str"</span><span class="p">,</span> <span class="s2">"c"</span> <span class="o">=&gt;</span> <span class="n">title</span> <span class="p">}]]</span>
<span class="p">}</span>
<span class="n">doc</span><span class="p">[</span><span class="s2">"blocks"</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">header</span><span class="p">]</span> <span class="o">+</span> <span class="n">blocks</span>

<span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span></code></pre></div></div>

<p>Save as <code class="language-plaintext highlighter-rouge">~/.config/apex/filters/title</code>, <code class="language-plaintext highlighter-rouge">chmod +x</code>, then <code class="language-plaintext highlighter-rouge">apex --filter title doc.md &gt; out.html</code>.</p>

<h3 id="short-lua-example">Short Lua example</h3>

<p>Same idea in Lua: read JSON, tweak <code class="language-plaintext highlighter-rouge">doc.blocks</code> (or <code class="language-plaintext highlighter-rouge">doc.meta</code>), write JSON. Requires <code class="language-plaintext highlighter-rouge">dkjson</code>.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="kd">local</span> <span class="n">json</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"dkjson"</span><span class="p">)</span>

<span class="kd">local</span> <span class="n">input</span> <span class="o">=</span> <span class="nb">io.read</span><span class="p">(</span><span class="s2">"*a"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">doc</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">doc</span> <span class="k">then</span>
  <span class="nb">io.stderr</span><span class="p">:</span><span class="n">write</span><span class="p">(</span><span class="s2">"Invalid JSON</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
  <span class="nb">os.exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1">-- Optional: transform doc.blocks or doc.meta</span>
<span class="c1">-- doc.blocks = ...</span>

<span class="nb">io.write</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">doc</span><span class="p">))</span></code></pre></div></div>

<p>Run it with <code class="language-plaintext highlighter-rouge">apex --lua-filter myfilter.lua input.md &gt; output.html</code>.</p>

<h3 id="is-this-feature-complete">Is This Feature Complete?</h3>

<p>No, but close. I can&rsquo;t get to 100% Pandoc compatibility at this point
without some restructuring of the AST generated by cmark-gfm. I&rsquo;m not
sure 100% parity is the goal at this point. So some existing Pandoc Lua
filters require some updates to work with Apex. Additionally, this
feature is literally what I came up with in two days and has a lot of
testing ahead. If you&rsquo;re willing to help, especially if you have Pandoc
filters you&rsquo;d like to port, please keep me posted on
<a href="https://github.com/ApexMarkdown/apex/issues">GitHub</a>.</p>

<h3 id="am-i-trying-to-replace-pandoc">Am I Trying to Replace Pandoc?</h3>

<p>No, really I&rsquo;m not. I&rsquo;m not trying to replicate Pandoc&rsquo;s amazing export abilities, or many of its extensions.</p>

<p>However, Pandoc&rsquo;s relatively vast compatibility with various flavors of Markdown is similar to what I want to do in Apex, so supporting many of its extensions makes sense, and supporting filters means users who&rsquo;ve written their own pipelines can easily switch to using Apex as a primary renderer. That&rsquo;s going to be important if I want Marked to have Pandoc capabilities without using Pandoc as an external dependency.</p>

<h3 id="learn-more">Learn more</h3>

<ul>
  <li><a href="https://pandoc.org/filters.html">Pandoc filters</a> — The JSON AST format and filter contract Apex follows.</li>
  <li><a href="https://github.com/ApexMarkdown/apex.wiki/blob/master/Filters.md">Apex wiki: Filters</a> — Full protocol, block/inline types, Lua details, and more examples.</li>
</ul>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116030178973650794">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Pandoc-style+filters+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F07%2Fpandoc-style-filters-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F07%2Fpandoc-style-filters-for-apex%2F&text=Pandoc-style+filters+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F07%2Fpandoc-style-filters-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17272052.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A trick for better naps]]></title>
    <link href="https://brett.trpstra.net/link/535/17270683/a-trick-for-better-naps"/>
    <updated>2026-02-05T12:51:00-06:00</updated>
    <published>2026-02-05T12:51:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/05/a-trick-for-better-naps</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/nap-routine-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve been sleeping anywhere between 4 and 6 hours a night, which isn&rsquo;t great because through trial and (mostly) error, I&rsquo;ve determined that at this point in my life, 8.5 hours of sleep is what I do best on. But naps help quite a bit.</p>

<blockquote class="tip">
  <p>By the way, if you want to follow my sleep journey as well as a couple of other neurodivergent people, along with fun pop culture and knowledge tech tidbits, you should listen to <a href="https://overtiredpod.com">Overtired</a>.</p>
</blockquote>

<p>As I&rsquo;ve <a href="https://brettterpstra.com/2026/02/04/project-updates-feb-4-2026-sleepless-edition/">mentioned before</a>, this sleep deprivation hasn&rsquo;t affected my coding productivity, but it&rsquo;s been a serious detriment to my social life, and affects my <a href="https://my.clevelandclinic.org/health/diseases/6004-dysautonomia">dysautonomia</a> (which I haven&rsquo;t talked about <a href="https://brettterpstra.com/2025/01/19/health-update-diagnoses/">for a while</a> but is still a major issue for me) and overall pain levels pretty drastically. My only saving grace has been daily naps.</p>

<h3 id="origin-of-my-routine">Origin of My Routine</h3>

<p>I&rsquo;ve always taken naps, even when sleeping well. I find it very rejuvenating and my afternoons are way more productive if I nap about an hour after lunch.</p>

<p>Once upon a time there was a startup called Napjitsu that sold a product appropriately called <em>Nap</em>. It was a combination of a chewable with mostly Valerian and 2 capsules containing mostly caffeine plus some other nootropics. You took them all at once. The chewable kicked in within 10 minutes, and the capsules metabolized about 30 minutes later, giving you a deep sleep for about 20 minutes and then waking up raring to go.</p>

<p>I loved those, but whatever else was in the capsules often made me wake up a bit &ldquo;shaky.&rdquo; Napjitsu shuttered a while back, but I found alternatives. This version of the routine doesn&rsquo;t leave me shaky. To the contrary, I wake up feeling quite energetic without side effects.</p>

<h3 id="scheduled-wakeup-call">Scheduled Wakeup Call</h3>

<p>For the caffeine, I&rsquo;m using <a href="https://amzn.to/4a0zzo9">capsules with 50mg of caffeine &amp; 100mg of L-Theanine</a> (these are Amazon affiliate links, which are helpful to me, but you can find these direct in other places, and I would support you <em>not supporting</em> Amazon). I only take one of those, as that&rsquo;s enough to get me going without making me jittery or affecting my sleep later that night.</p>

<p>You can really just drink a cup of coffee or a shot of espresso before laying down, as <a href="https://en.wikipedia.org/wiki/Caffeine">caffeine</a> in general takes 30 minutes to metabolize. But if you want the combination of caffeine and L-Theanine, that&rsquo;s basically tea. Just drink a cup of tea. (White or green tea will give you approximately the right amount of caffeine, with white tea capping out at 55mg and green tea capping out at 70mg.) The capsules aren&rsquo;t the key, just the caffeine.</p>

<p>By the way, if you ever see guaranine as an ingredient in energy drinks or supplements, it&rsquo;s essentially just caffeine. <a href="https://en.wikipedia.org/wiki/Guarana">Guarana</a> is a plant, and guaranine is derived from it&rsquo;s seeds. It includes some other compounds, but the only one of note is caffeine.</p>

<h3 id="sleep-phase">Sleep Phase</h3>

<p>For the sleep phase of the nap, I tried a few things, but valerian root gives me the best results.</p>

<p><a href="https://www.sleepfoundation.org/sleep-aids/valerian-root">Valerian root</a> is primarily indicated as a sleep aid. It&rsquo;s rumored to be good for stress, PMS, and anxiety, but there&rsquo;s no scientific evidence for those. It&rsquo;s also commonly used as a tea, so if you really want to drink a couple of cups of tea before laying down, that&rsquo;s an option, but you&rsquo;ll have to pee, what with liquids and diuretics and all. A need to urinate might help you get back up, too, but I personally dislike waking up like that.</p>

<p>Velerian root offers me fast sedation, but easy to overcome with stimulants like caffeine, unlike supplements like melatonin. I&rsquo;m using gummies with <a href="https://amzn.to/3OnYUQm">6000mg of valerian root and chamomile</a>. I take 2-4 of those, depending on how overtired I am. I don&rsquo;t know about the safety of doubling the recommended serving size (2 gummies), but 4 does the trick on days I feel wired despite (or because of) lack of sleep.</p>

<p>Weirdly, napping is easiest for me on days I actually <em>did</em> get enough sleep, which happens every 7-10 days. But on the days I most need a nap, a little supplemental cocktail is both necessary and effective.</p>

<p>I don&rsquo;t know if this routine is a good idea, or how well it will work for others. I can&rsquo;t guarantee anything. But I can anecdotally endorse it as a decent way to &ldquo;force&rdquo; a restful nap that doesn&rsquo;t leave me dragging for the afternoon. If you want to nap but have trouble dozing off, or have trouble waking up, or both, this might be worth a try.</p>

<h3 id="hows-your-sleep">How&rsquo;s Your Sleep?</h3>

<p>I hope that&rsquo;s helpful to others 💤. Let me know <a href="https://forum.brettterpstra.com">in the forum</a> if you have any of your own tips, I&rsquo;m always looking for new ideas in this area.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116019596506298906">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=A+trick+for+better+naps%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F05%2Fa-trick-for-better-naps%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F05%2Fa-trick-for-better-naps%2F&text=A+trick+for+better+naps&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F05%2Fa-trick-for-better-naps%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17270683.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/nap-routine-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/nap-routine-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Project Updates Feb 4, 2026 — Sleepless Edition]]></title>
    <link href="https://brett.trpstra.net/link/535/17269961/project-updates-feb-4-2026-sleepless-edition"/>
    <updated>2026-02-04T09:24:00-06:00</updated>
    <published>2026-02-04T09:24:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/04/project-updates-feb-4-2026-sleepless-edition</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/project-updates-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I&rsquo;ve been hard at work on a bunch of projects at once, so this is partly just a personal check-in on how things are going. Thanks for being my therapist of sorts. So here&rsquo;s what&rsquo;s up with current projects, including Marked 3, Howzit, Apex, and more.</p>

<ul class="toc">
  <li>Table of Contents</li>
</ul>

<h3 id="marked-3">Marked 3</h3>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/marked-icon.png"></p>
<p>The <a href="https://markedapp.com/join-the-beta">Marked beta</a> continues. It&rsquo;s going really well, and I believe I&rsquo;ve fixed all crashes and made a bunch of improvements. One thing the next release will feature is better large document handling. This was important for dealing with BlogBook (see below) exports, but is also a vital quality of life improvement. Now the loading doesn&rsquo;t hang while reading 1MB+ files, and the rendering is faster for all processors.</p>

<p>I added full tab handling (finally), with the option to open new documents in tabs. Plus a <a href="https://markedapp.com/help/Quick_Open">Quick Open</a> feature that lets you hit <span class="keycombo separated" title="Shift-Command-O"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">O</kbd></span> and quickly type or use the arrow keys to jump to a window, tab, or recent document.</p>

<p>I also included <a href="https://markedapp.com/help/Command_Line_Utility">a CLI</a> that makes accessing all of Marked&rsquo;s URL scheme methods easy from the command line. It can&rsquo;t yet automate a full export, but it&rsquo;s still useful. I&rsquo;ve refactored the whole export system to eventually allow full automation, but that&rsquo;s going to be a 3.1 feature, not something that holds up the 3.0 release.</p>

<h3 id="blogbook">BlogBook</h3>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/blogbook-icon.png"></p>
<p>This is <a href="https://blogbook.my">a new app</a> I&rsquo;m publishing as a complement to Marked 3. It generates a &ldquo;Book&rdquo; from WordPress, Micro.blog, or Ghost blogs. You can filter posts by author, categories, tags, date ranges, and more, then export a Markdown document (or collection of documents with an index file) which you can open in any Markdown editor. If you open it in Marked 3, special syntax like Table of Contents and page breaks will be rendered, and you can output to PDF, HTML, EPUB, and more.</p>

<p>This will be a great tool for people who have been blogging for a few (or a lot of) years who want a more permanent record of their work. Websites are known to disappear, but a good EPUB on your backup drive means your content will never be lost. Store your content as text, PDF, or EPUB for posterity, and/or distribute (or sell) a PDF/Ebook version of your writings. The filtering means you can easily export just a certain type of post, export only a certain date range, and you can even have the export split by year or month for making smaller books.</p>

<p>Check out the documentation at <a href="https://blogbook.my">blogbook.my</a> to see what it looks like. If you&rsquo;re interested in helping me test, a TestFlight build will be available soon. It will be invite-only, so if you&rsquo;re interested and have a WordPress, Micro.blog, or Ghost blog, <a href="https://brettterpstra.com//contact">contact me directly</a> for an invite.</p>

<h3 id="apex">Apex</h3>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/apex-icon.png"></p>
<p>The last few releases of Apex have mostly addressed issues, but I added a couple new things.</p>

<p>First, I added support for Leanpub index syntax: <code class="language-plaintext highlighter-rouge">{i: term}</code>, <code class="language-plaintext highlighter-rouge">{i: "term"}</code>, and <code class="language-plaintext highlighter-rouge">{i: "Main!sub"}</code> for hierarchical entries under headings. This won&rsquo;t be of use to anyone but Leanpub authors, but adding it to the existing handling of mmark and TextIndex syntax was fairly trivial.</p>

<p>Second, a feature I really like: if you add a <code class="language-plaintext highlighter-rouge">@2x</code> attribute anywhere in image attributes, e.g. <code class="language-plaintext highlighter-rouge">![ALT](/image.png @2x)</code>, it will generate an image with a srcset containing the original image at 1x, and then the image with @2x appended before the extension for retina displays. I also added support for <code class="language-plaintext highlighter-rouge">@3x</code>, which will generate srcset entries for 1x, 2x, and 3x variants of the same path.</p>

<p>This srcset feature is one I&rsquo;ve always wanted, as I almost always provide @2x assets but Markdown never made it easy to generate the appropriate markup for these. I&rsquo;m doing my best not to add arbitrary syntax to Apex that doesn&rsquo;t exist anywhere else, but this one I really needed.</p>

<p>I also did my first real-world conversion of a build system to Apex. The Marked documentation has always used MultiMarkdown, with a build script that manually converts a bunch of Liquid-style tags (like my <code class="language-plaintext highlighter-rouge">kbd</code> plugin and one that generates menu item notations that open Settings panes in Marked). I converted the whole thing to use Apex, and wrote project-level plugins for all of the custom syntax I wanted to handle. It worked perfectly and required no changes to my existing documentation. So I can vouch that Apex is a great tool for this kind of thing.</p>

<p>I haven&rsquo;t pulled the trigger on integrating Apex into Marked yet, but thanks to some reported Issues, I&rsquo;ve got the Xcode integration working well. Apex is on the verge of a 1.0 release, at which point I <em>will</em> be converting Marked to use it. If you&rsquo;re a Mac or iOS developer of a Markdown app and are willing to try it out, I&rsquo;d love to help you integrate Apex and am very interested in getting some traction via inclusion in Mac/iOS apps. It&rsquo;s pure C, so if you&rsquo;re developing on other platforms, it will work just about anywhere, I just have the most interest in Apple platforms.</p>

<h4 id="apex-fixes">Apex Fixes</h4>

<ul>
  <li>TextIndex <code class="language-plaintext highlighter-rouge">[term]{^}</code> no longer includes the closing bracket in the index entry (e.g. &ldquo;fresh&rdquo; instead of &ldquo;fresh]&rdquo;).</li>
  <li>Reference-style image attributes (width, height, style, classes, id) are correctly applied in Unified, MultiMarkdown, and GFM modes, even when mixed with inline images and fenced div/figure blocks</li>
</ul>

<h3 id="howzit">Howzit</h3>

<p>I made a simple update to <a href="https://brettterpstra.com/projects/howzit/" title="howzit">Howzit</a> to allow some interesting shell integration.</p>

<p>The <code class="language-plaintext highlighter-rouge">--test-search</code> command just returns a zero exit code if:</p>

<ol>
  <li>A build note file is found (<code class="language-plaintext highlighter-rouge">build*.{md,txt}</code>)</li>
  <li>The search string given matches at least one topic, based on configured search type</li>
</ol>

<p>If either of those fails, then it returns a non-zero exit code.</p>

<p>This means you can have a <a href="https://github.com/ttscoff/howzit/wiki/Shell-integration#using-topics-as-shell-commands-fish-and-zsh">Fish or Zsh function for handling unknown commands</a> by testing for a matching topic and executing that if the command isn&rsquo;t found and a matching build note topic exists. So if I have a &ldquo;Run Tests&rdquo; topic in my build notes, and I run <code class="language-plaintext highlighter-rouge">runt</code> on the command line, the unknown command function will execute and will run <code class="language-plaintext highlighter-rouge">howzit -r runt</code>, which will match the &ldquo;Run Tests&rdquo; topic (if fuzzy matching is enabled).</p>

<h3 id="wordpress-plugins">WordPress Plugins</h3>

<p>I also added some localization to the <a href="https://brettterpstra.com/2026/02/02/a-couple-new-wordpress-plugins/">WordPress plugins</a> I shared the other day. No major changes to functionality, just language files for German, Spanish, French, and Italian. These were generated with AI, so I can&rsquo;t vouch for their accuracy. If any speakers of these languages care to look through the various PO files for <a href="https://github.com/ttscoff/bt-downloads/tree/main/bt-downloads/languages">bt-downloads</a> and <a href="https://github.com/ttscoff/bt-keyboard-shortcuts/tree/main/bt-keyboard-shortcuts/languages">bt-keyboard-shortcuts</a>, feel free to let me know (and I&rsquo;ll credit you in the documentation). Eventually when I get these published to the WordPress directory<sup id="fnref:wpdirectory"><a href="https://brettterpstra.com#fn:wpdirectory" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, this will be especially useful.</p>

<p>I should probably at least add Simplified Chinese, too. I&rsquo;ll think about that. Any other language suggestions?</p>

<h3 id="on-multitasking">On Multitasking</h3>

<p>That pretty well sums up what I&rsquo;m working on right now. My sleep cycle is still only allowing me about 5 hours of sleep per night, and I&rsquo;m usually up coding by 3:30AM. And when I&rsquo;m really tired I have this weird ability to focus better while simultaneously multitasking, so most mornings I have 3 or 4 projects open at once and switch between them while one compiles or run tests or I just get bored. That part is kind of cool, though I&rsquo;d rather be sleeping, were that an option.</p>

<p>The reason I keep adding to Howzit is because it&rsquo;s how I manage to jump between projects. With identically-named topics between the projects, when switching contexts I don&rsquo;t have to remember which build system I&rsquo;m supposed to use, I just use Howzit to develop, run tests, build/compile, and deploy. And taking copious notes in my build notes files means I can quickly query a topic and remember what exactly I was doing.</p>

<p>And then there&rsquo;s <a href="https://brettterpstra.com//projects/doing">Doing</a>, which I have attached to git hooks in every repo. Every time I make a commit, it adds a Doing entry for me, so if at 4am I suddenly blank on what I was doing at 3:30, I can just run <code class="language-plaintext highlighter-rouge">doing since 3:30am</code> to see what I&rsquo;ve been working on. It&rsquo;s <em>super</em> handy, and since I&rsquo;m constantly committing to my git repos, I don&rsquo;t even have to worry about making manual Doing entries.</p>

<p>Hopefully something in this update was of interest to you. Please keep me
posted on what you&rsquo;re using (<a href="https://forum.brettterpstra.com">join us in the forum!</a>), what questions/issues/requests you have,
and, as we we always say on Overtired, get some sleep, if you can.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:wpdirectory">
      <p>My BT-SVG-Viewer plugin has been stuck in review for what feels like months, and you can only submit one plugin at a time.&nbsp;<a href="https://brettterpstra.com#fnref:wpdirectory" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116013154871074263">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Project+Updates+Feb+4%2C+2026+%E2%80%94+Sleepless+Edition%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F04%2Fproject-updates-feb-4-2026-sleepless-edition%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F04%2Fproject-updates-feb-4-2026-sleepless-edition%2F&text=Project+Updates+Feb+4%2C+2026+%E2%80%94+Sleepless+Edition&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F04%2Fproject-updates-feb-4-2026-sleepless-edition%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17269961.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/project-updates-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/project-updates-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Karabiner Home row app switcher]]></title>
    <link href="https://brett.trpstra.net/link/535/17269248/home-row-app-switcher"/>
    <updated>2026-02-03T09:23:00-06:00</updated>
    <published>2026-02-03T09:23:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/03/home-row-app-switcher</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/keyboard-header-green-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Here&rsquo;s a new Karabiner-Elements trick that I&rsquo;ve been using to add home row app switching to my workflow. Tools like <a href="https://github.com/mikker/LeaderKey" title="mikker/LeaderKey:The *faster than your launcher* launcher">LeaderKey</a> are great, but sometimes you just want the App Switcher, and sometimes you just want to be lazy with your fingers.</p>

<p>My goal was a home-row app switcher that triggered without leaving the home row, and didn&rsquo;t steal keys for normal typing. The solution is a Karabiner-Elements complex modification that uses <strong>simultaneous</strong> key detection: the layer only activates when <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> and <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span> are pressed <em>together</em> within a very short window (basically at the exact same time).</p>

<p><strong>This modification is designed for the <a href="https://www.keyboardmaestro.com/">Keyboard Maestro</a> app switcher</strong>, not the macOS default <span class="keycombo separated" title="Command-Tab Key"><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">&#8677;</kbd></span> switcher. Keyboard Maestro&rsquo;s switcher uses Shift alone to move backward, whereas the system switcher expects <span class="keycombo separated" title="Shift-Command-Tab Key"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">&#8677;</kbd></span>; the key outputs below match KM&rsquo;s behavior, so you&rsquo;d need to adjust the modification (e.g. have <span class="keycombo separated" title="D"><kbd class="key symbol">D</kbd></span> send <span class="keycombo separated" title="Shift-Tab Key"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">&#8677;</kbd></span>) to use it with the built-in app switcher.</p>

<h2 id="what-it-does">What it does</h2>

<p>Hold <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> and <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span> at the same time. While both are held, you get a temporary layer:</p>

<ul>
  <li><strong><span class="keycombo separated" title="D"><kbd class="key symbol">D</kbd></span></strong> &rarr; previous app (Shift only; KM uses this for backward)</li>
  <li><strong><span class="keycombo separated" title="F"><kbd class="key symbol">F</kbd></span></strong> &rarr; next app (<span class="keycombo separated" title="Command-Tab Key"><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">&#8677;</kbd></span>)</li>
  <li><strong><span class="keycombo separated" title="G"><kbd class="key symbol">G</kbd></span></strong> &rarr; quit (<span class="keycombo separated" title="Command-Q"><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">Q</kbd></span>)</li>
</ul>

<p>So you chord <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span>+<span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span>, then tap <span class="keycombo separated" title="F"><kbd class="key symbol">F</kbd></span>, it loads the App Switcher, then tap <span class="keycombo separated" title="D"><kbd class="key symbol">D</kbd></span> or <span class="keycombo separated" title="F"><kbd class="key symbol">F</kbd></span> to move through the apps, or <span class="keycombo separated" title="G"><kbd class="key symbol">G</kbd></span> to quit the highlighted app. Release <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> and/or <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span> and switch to the highlighted app.</p>

<p>This works great with my <a href="https://brettterpstra.com/2025/03/24/keybindings-home-row-arrow-cluster/">home row arrow keys</a>, allowing easy up/down/left/right navigation through Keyboard Maestro&rsquo;s grid-based app switcher. Two-handed, but still all home row.</p>

<h2 id="how-it-avoids-hijacking-a-and-s-for-regular-typing">How it avoids hijacking A and S for regular typing</h2>

<p>The modification never intercepts a lone <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> or <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span>. It only triggers when both keys are pressed <strong>simultaneously</strong>, with a 50 ms threshold (<code class="language-plaintext highlighter-rouge">basic.simultaneous_threshold_milliseconds</code>). When you type normally, you press one key, release it, then press another. Your &ldquo;a&rdquo; and &ldquo;s&rdquo; key events are separated by hundreds of milliseconds, so Karabiner never sees them as a chord. Only an intentional A+S chord activates the layer. Single-key &ldquo;a&rdquo; and &ldquo;s&rdquo; pass through unchanged.</p>

<p>Under the hood, the rule uses Karabiner&rsquo;s <code class="language-plaintext highlighter-rouge">simultaneous</code> <code class="language-plaintext highlighter-rouge">from</code> condition: an array of key codes that must all be pressed together. On that chord it sets a variable (<code class="language-plaintext highlighter-rouge">as_layer</code>) and injects <span class="keycombo separated" title="Left_command"><kbd class="key symbol">left_command</kbd></span> so that the following D/F/G rules (which are gated on <code class="language-plaintext highlighter-rouge">as_layer</code>) produce the right modifiers. When either A or S is released, <code class="language-plaintext highlighter-rouge">to_after_key_up</code> clears the variable and the layer drops. So you keep full use of <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> and <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span> for typing; only the deliberate two-finger chord turns them into a layer key.</p>

<h2 id="import-this-modification">Import this modification</h2>

<div class="biner-wrap" data-json-url="https://brettterpstra.com/karabiner/modifications/home-row-app-switcher.json">
  <div class="biner-btn-group">
    <a href="https://brettterpstra.comkarabiner://karabiner/assets/complex_modifications/import?url=https%3A%2F%2Fbrettterpstra.com%2Fkarabiner%2Fmodifications%2Fhome-row-app-switcher.json" class="biner-btn-main">Import to Karabiner</a>
    <button type="button" class="biner-btn-dropdown-toggle" aria-expanded="false" aria-haspopup="true" title="More options">
      <span class="biner-chevron" aria-hidden="true">&#9660;</span>
    </button>
  </div>
  <div class="biner-dropdown-menu" role="menu" hidden="">
    <button type="button" class="biner-dropdown-item biner-show-json" role="menuitem">Show JSON</button>
    <button type="button" class="biner-dropdown-item biner-copy-url" role="menuitem">Copy JSON URL</button>
  </div>
</div>
<script>
(function(){
  if (!window.BinerModal) {
    window.BinerModal = (function(){
      var overlay, pre, currentText;
      function closeModal(){ if (overlay) { overlay.setAttribute("hidden", ""); overlay.setAttribute("aria-hidden", "true"); document.removeEventListener("keydown", escHandler); } }
      function escHandler(e){ if (e.key === "Escape") closeModal(); }
      function createModal() {
        overlay = document.createElement('div');
        overlay.className = 'biner-modal-overlay';
        overlay.setAttribute('aria-hidden', 'true');
        overlay.innerHTML = '<div class="biner-modal-box" role="dialog" aria-modal="true" aria-labelledby="biner-modal-title">' +
          '<div class="biner-modal-header">' +
            '<h2 id="biner-modal-title" class="biner-modal-title">Modification JSON</h2>' +
            '<button type="button" class="biner-modal-close" aria-label="Close">&times;</button>' +
          '</div>' +
          '<div class="biner-modal-body">' +
            '<pre class="biner-modal-pre"></pre>' +
          '</div>' +
          '<div class="biner-modal-footer">' +
            '<button type="button" class="biner-modal-btn biner-modal-copy">Copy</button>' +
            '<button type="button" class="biner-modal-btn biner-modal-download">Download</button>' +
          '</div></div>';
        pre = overlay.querySelector('.biner-modal-pre');
        var style = document.createElement('style');
        style.textContent = '.biner-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10000;padding:20px;}.biner-modal-overlay[hidden]{display:none!important}.biner-modal-box{background:#fff;border-radius:8px;box-shadow:0 8px 32px rgba(0,0,0,0.2);max-width:90vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden}.biner-modal-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #eee}.biner-modal-title{margin:0;font-size:1.1rem;font-weight:600}.biner-modal-close{background:none;border:none;font-size:24px;line-height:1;cursor:pointer;color:#666;padding:0 4px}.biner-modal-close:hover{color:#000}.biner-modal-body{flex:1;overflow:auto;padding:16px}.biner-modal-pre{margin:0;font-family:monospace;font-size:13px;line-height:1.5;white-space:pre-wrap;word-break:break-all}.biner-modal-footer{padding:12px 16px;border-top:1px solid #eee;display:flex;gap:8px;justify-content:flex-end}.biner-modal-btn{padding:8px 16px;border-radius:6px;border:1px solid #ccc;background:#f5f5f5;cursor:pointer;font:inherit}.biner-modal-btn:hover{background:#e5e5e5}.biner-modal-copy:focus,.biner-modal-download:focus{outline:2px solid #007aff;outline-offset:2px}';
        document.head.appendChild(style);
        overlay.querySelector('.biner-modal-close').addEventListener('click', closeModal);
        overlay.addEventListener('click', function(e){ if (e.target === overlay) closeModal(); });
        overlay.querySelector('.biner-modal-copy').addEventListener('click', function(){
          if (!currentText) return;
          if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(currentText);
          } else {
            var ta = document.createElement('textarea'); ta.value = currentText; ta.setAttribute('readonly', ''); ta.style.position = 'absolute'; ta.style.left = '-9999px';
            document.body.appendChild(ta); ta.select();
            try { document.execCommand('copy'); } finally { document.body.removeChild(ta); }
          }
        });
        overlay.querySelector('.biner-modal-download').addEventListener('click', function(){
          if (!currentText) return;
          var fn = (overlay.getAttribute('data-download-filename') || 'modification.json');
          var blob = new Blob([currentText], { type: 'application/json' });
          var url = URL.createObjectURL(blob);
          var a = document.createElement('a'); a.href = url; a.download = fn; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
        });
        document.body.appendChild(overlay);
      }
        return {
        show: function(jsonUrl) {
          if (!overlay) createModal();
          overlay.removeAttribute('hidden');
          overlay.setAttribute('aria-hidden', 'false');
          document.addEventListener('keydown', escHandler);
          pre.textContent = 'Loading…';
          currentText = null;
          overlay.setAttribute('data-download-filename', (jsonUrl.split('/').pop() || 'modification.json'));
          fetch(jsonUrl).then(function(r){ if (!r.ok) throw new Error(r.status); return r.text(); }).then(function(text){
            try { text = JSON.stringify(JSON.parse(text), null, 2); } catch (e) {}
            currentText = text;
            pre.textContent = text;
          }).catch(function(){
            pre.textContent = 'Failed to load JSON.';
          });
        }
      };
    })();
  }
  var wrap = document.currentScript.previousElementSibling;
  var toggle = wrap.querySelector('.biner-btn-dropdown-toggle');
  var menu = wrap.querySelector('.biner-dropdown-menu');
  var jsonUrl = wrap.getAttribute('data-json-url');
  function closeMenu(){ menu.hidden = true; toggle.setAttribute('aria-expanded', 'false'); }
  function openMenu(){ menu.hidden = false; toggle.setAttribute('aria-expanded', 'true'); }
  toggle.addEventListener('click', function(e){
    e.preventDefault();
    e.stopPropagation();
    menu.hidden ? openMenu() : closeMenu();
  });
  wrap.querySelector('.biner-show-json').addEventListener('click', function(){
    closeMenu();
    window.BinerModal.show(jsonUrl);
  });
  wrap.querySelector('.biner-copy-url').addEventListener('click', function(){
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(jsonUrl).then(closeMenu);
    } else {
      var ta = document.createElement('textarea');
      ta.value = jsonUrl;
      ta.setAttribute('readonly', '');
      ta.style.position = 'absolute';
      ta.style.left = '-9999px';
      document.body.appendChild(ta);
      ta.select();
      try { document.execCommand('copy'); closeMenu(); } catch (err) {}
      document.body.removeChild(ta);
    }
  });
  document.addEventListener('click', function(e){
    if (!wrap.contains(e.target)) closeMenu();
  });
})();
</script>

<div id="paywall_8743d3287caa2902" class="paywall-container" data-paywall-id="paywall_8743d3287caa2902" data-content-id="8743d3287caa2902">
  <div class="paywall-content-placeholder" style="display: none;"></div>
  <div class="paywall-overlay">
    <div class="paywall-message">
      <div class="paywall-icon">🔒</div>
      <h3>Premium Content</h3>
      <p>And More</p>
      <div class="paywall-actions">
        <a href="https://brettterpstra.com//support/" class="paywall-subscribe-btn">Subscribe Now</a>
        <button class="paywall-login-btn" onclick="paywallLogin()">I'm a Subscriber</button>
      </div>
    </div>
  </div>
</div>

<script>
  // Set up global paywall config and registration queue
  (function() {
    if (!window.paywallConfig) {
      window.paywallConfig = {
        serviceUrl: 'https://paywall.brettterpstra.com',
        cookieName: 'bt_member_token',
        instances: []
      };
    }

    // Register this instance (collected by bt.Paywall when it loads)
    window.paywallConfig.instances.push({
      id: 'paywall_8743d3287caa2902',
      contentId: '8743d3287caa2902'
    });
  })();
</script>


<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116007497228558733">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Karabiner+Home+row+app+switcher%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F03%2Fhome-row-app-switcher%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F03%2Fhome-row-app-switcher%2F&text=Karabiner+Home+row+app+switcher&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F03%2Fhome-row-app-switcher%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17269248.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/keyboard-header-green-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/keyboard-header-green-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A couple new WordPress plugins]]></title>
    <link href="https://brett.trpstra.net/link/535/17269249/a-couple-new-wordpress-plugins"/>
    <updated>2026-02-02T13:00:00-06:00</updated>
    <published>2026-02-02T13:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/02/a-couple-new-wordpress-plugins</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/wordpress-plugins-header-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Just for fun, I&rsquo;ve been porting some of my Jekyll plugins to
WordPress, and two of them have turned into what I think are
really useful plugins.</p>

<p>I&rsquo;m not switching from Jekyll to WordPress, I&rsquo;ve just been
enjoying the challenge of recreating these tools as
WordPress extensions. It&rsquo;s somewhat funny because the Jekyll
plugins were originally created from WordPress plugins I&rsquo;d
built, and they developed over time. So this is kind of a
round trip.</p>

<h3 id="bt-downloads">BT Downloads</h3>

<p><a href="https://brettterpstra.com//projects/bt-downloads">Project page</a></p>

<p>A plugin for managing downloadables and dropping download
cards into posts. You get a custom post type for each
download (file URL, version, description, info link, icon,
changelog), upload buttons for the file and icon right on
the edit screen, and an editable HTML card template with
Mustache-style conditionals
(<code class="language-plaintext highlighter-rouge">{{#description}}...{{/description}}</code>) plus custom CSS with
a live preview.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/downloads-800.jpg"></p>

<p>Insert them via a TinyMCE button in the classic editor or a
<strong>Download</strong> block in the block editor. Pick from a dropdown
and the shortcode (or block) is inserted for you. On the
frontend you get a styled card: title, download link,
description, dates, and optional donate/info links. There&rsquo;s
also a WP-CLI command to import from CSV (the way I handle
downloads in Jekyll):
<code class="language-plaintext highlighter-rouge">wp btdl import_downloads --file=/path/to/downloads.csv</code> .
Full details, more screenshots, and the shortcode reference
are on the <a href="https://brettterpstra.com//projects/bt-downloads">BT Downloads project page</a>.</p>

<h3 id="bt-keyboard-shortcuts">BT Keyboard Shortcuts</h3>

<p><a href="https://brettterpstra.com//projects/bt-keyboard-shortcuts">Project page</a></p>

<p>This one is for writing keyboard shortcuts in posts without
hand-coding symbols. I&rsquo;ve created a few variations of this
over time: <a href="https://brettterpstra.com/2021/06/22/a-jekyll-plugin-for-documenting-mac-keyboard-shortcuts/">for this blog</a>, for Marked and Bunch
documentation, and probably others. A shortcode <code class="language-plaintext highlighter-rouge">[kbd]</code>
renders things like <span class="keycombo separated" title="Option-Shift-Command-A"><kbd class="mod symbol">&#8997;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">A</kbd></span> in the order Apple
recommends, with options for symbol vs text (e.g.
&ldquo;Command-Shift-P&rdquo;), a + separator, and Mac vs Windows
naming.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/kbd-shortcut-editor.jpg"></p>

<p>In the block or classic editor, use the formatting menu and
choose <strong>⌘ Insert keyboard shortcut</strong> to open a dialog:
check modifiers (Cmd/Opt/Shift/Ctrl/Fn), type the key, and
insert the shortcode. Under <strong>Settings → Keyboard
Shortcuts</strong> you can tweak display (symbols vs text, +
separator, key symbols) and add custom CSS for the <code class="language-plaintext highlighter-rouge">.btkbd</code>
keycaps, with a live preview. Examples: <code class="language-plaintext highlighter-rouge">[kbd cmd shift p]</code>
&rarr; <span class="keycombo separated" title="Shift-Command-P"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">P</kbd></span>, <code class="language-plaintext highlighter-rouge">[kbd$@P]</code> → <span class="keycombo separated" title="Shift-Command-P"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">P</kbd></span>. More
syntax and options are on the
<a href="https://brettterpstra.com//projects/bt-keyboard-shortcuts">BT Keyboard Shortcuts project page</a>.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/kbd-post.jpg"></p>

<h3 id="repo-and-requirements">Repo and requirements</h3>

<p>Both require WordPress 5.8+ and PHP 7.4+, and are GPLv2 or
later. The source for these and any future WordPress plugins
from me is my <a href="https://github.com/ttscoff/wordpress-plugins">wordpress-plugins repo on GitHub</a>. Grab
the code there or follow the install steps on each project
page.</p>


<p>Like or share this post <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fa-couple-new-wordpress-plugins%2F&text=A+couple+new+WordPress+plugins&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fa-couple-new-wordpress-plugins%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17269249.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/wordpress-plugins-header-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/wordpress-plugins-header-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for February 2nd, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17268512/web-excursions-for-february-2nd-2026"/>
    <updated>2026-02-02T10:14:00-06:00</updated>
    <published>2026-02-02T10:14:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/02/web-excursions-for-february-2nd-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Whether you&rsquo;re a new user or a seasoned pro, ScreenCastsONLINE offers in-depth screencasts on a wide range of topics, from tutorials to app discovery. <a href="https://screencastsonline.com/members/aff/go/bterpstra">Check it out.</a></p>

<dl>
  <dt><a href="https://iloveapiano.app/">I Love a Piano</a></dt>
  <dd>I met Max Mellman at Macstock and we became fast friends. His first app is a piano synth with multiple on-screen scrollable keyboards, different piano sounds, and a built in music player with fine scrubbing for learning along with your favorite music.</dd>
  <dt><a href="https://www.amazon.com/Lawyers-Guide-Podcasting-Tech-Savvy-Lawyer-Pages/dp/B0GGX32DZH#detailBullets_feature_div">The Lawyer&rsquo;s Guide to Podcasting</a></dt>
  <dd>A book from my friend Michael D.J. Eisenberg ((The Tech-Savvy Lawyer): The Lawyer&rsquo;s Guide to Podcasting: Building Your Brand, Audience, Tech Stack, and Expertise!</dd>
  <dt><a href="https://github.com/rhsev/mi.lan">rhsev/mi.lan:</a></dt>
  <dd>A lightweight URL bridge for macOS automation, and a companion for <a href="https://github.com/rhsev/dy.lan">dy.lan</a>, which I <a href="https://brettterpstra.com/2026/01/27/a-url-router-that-turns-your-local-network-into-a-workflow-engine/">wrote about last week</a>.</dd>
  <dt><a href="https://apps.apple.com/us/app/vimari/id1480933944?mt=12">‎Vimari App - App Store</a></dt>
  <dd>A port of Vimium for Safari. Vimium is one of my favorite extensions on Chrome and Firefox, and I&rsquo;d always missed it on Safari, not realizing someone had already ported it.</dd>
  <dt><a href="https://apps.apple.com/us/app/linkthing/id1666031776">‎LinkThing</a></dt>
  <dd>Not perfect, but if you need to view and search your linkding bookmarks, this runs on all Apple platforms and does the trick.</dd>
  <dt><a href="https://github.com/moltbot/moltbot">moltbot/moltbot: Your own personal AI assistant.</a></dt>
  <dd>A personal chatbot assistant for any OS and any platform. I think this is what I&rsquo;m going to do with one of the 2012 Mac minis I picked up for $25.</dd>
  <dt><a href="https://github.com/steveyegge/gastown">Gas Town - multi-agent workspace manager</a></dt>
  <dd>We talked about this at some length on the <a href="https://overtiredpod.com/ep/442/">last Overtired</a>. Run multiple AI coding agents simultaneously with lots of useful features. Not cheap to implement fully, but powerful.</dd>
  <dt><a href="https://updates.techforpalestine.org/upscrolled-is-live-the-instagram-alternative-thats-actually-on-your-side/">UpScrolled is live</a></dt>
  <dd>
    <blockquote>
      <p>If you&rsquo;ve been waiting for a real alternative to Instagram, this is it.</p>
    </blockquote>
  </dd>
  <dd>
    <p>From a Palestinian-Australian developer and supported by Tech for Palestine, a solid looking alternative to Instagram that allows text updates and no shadow banning/censorship. Right now it&rsquo;s mostly my source for news from Gaza, but I&rsquo;m curious to see how it grows. They&rsquo;ve already hit limitations of scale and are updating rapidly.</p>
  </dd>
</dl>

<p>Want more great tips and apps? Check out <a href="https://screencastsonline.com/members/aff/go/bterpstra">ScreenCastsOnline</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116001986004813901">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+February+2nd%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fweb-excursions-for-february-2nd-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fweb-excursions-for-february-2nd-2026%2F&text=Web+Excursions+for+February+2nd%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fweb-excursions-for-february-2nd-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17268512.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Project-level plugins and config for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17265388/project-level-plugins-and-config-for-apex"/>
    <updated>2026-01-28T12:14:00-06:00</updated>
    <published>2026-01-28T12:14:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/28/project-level-plugins-and-config-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>If you use Apex for more than one project, you&rsquo;ve probably hit the point where a single global setup doesn&rsquo;t quite cut it. Maybe your book project wants a different plugin set than your docs site, or one repo has stricter defaults than everything else on your machine.</p>

<p>As a personal example, most of my app documentation has always been written in MultiMarkdown, where header ids get generated with no spaces or dashes, so all of my cross-references link to <code class="language-plaintext highlighter-rouge">#thissection</code> type of anchors. My blog and my Jekyll-based documentation sites have always used Kramdown, so header ids and cross references are <code class="language-plaintext highlighter-rouge">#this-section</code>. I needed an easy way to always have Apex use the right header format for the current project.</p>

<p>I also have special plugins for different destinations. For example, my Marked documentation has special Liquid-style tags like <code class="language-plaintext highlighter-rouge">prefpane</code> that generates nice HTML for referencing Preference panes with <code class="language-plaintext highlighter-rouge">x-marked</code> URLs that will open a preference pane directly in Marked. I don&rsquo;t need or want a plugin to do that universally, the output it generates is very specific to Marked.</p>

<p>So I added project-scoped plugins and configurations to Apex. This allows me to get the settings just right for a project, then save them into a local directory and be able to just run <code class="language-plaintext highlighter-rouge">apex</code> without a bunch of command line flags to remember.</p>

<p>You also get a cleaner way to &ldquo;shadow&rdquo; plugins you don&rsquo;t want with a local noop plugin.</p>

<blockquote>
  <p>I also added <code class="language-plaintext highlighter-rouge">++insert++</code> syntax for adding <code class="language-plaintext highlighter-rouge">&lt;ins&gt;insert&lt;/ins&gt;</code> tags, but that&rsquo;s just a little one-off addition.</p>
</blockquote>

<h3 id="project-scoped-plugins-in-apexplugins">Project-scoped plugins in <code class="language-plaintext highlighter-rouge">.apex/plugins</code></h3>

<p>Plugins used to be purely global: Apex would only look in your XDG config dir:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/apex/plugins</code>, or</li>
  <li><code class="language-plaintext highlighter-rouge">~/.config/apex/plugins</code></li>
</ul>

<p>Now there&rsquo;s a proper <strong>project scope</strong>, searched in this order:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">./.apex/plugins</code> (current working directory)</li>
  <li><code class="language-plaintext highlighter-rouge">BASE/.apex/plugins</code> when you run with <code class="language-plaintext highlighter-rouge">--base-dir BASE</code></li>
  <li><code class="language-plaintext highlighter-rouge">&lt;git-root&gt;/.apex/plugins</code> when you&rsquo;re inside a Git work tree</li>
  <li>Global: <code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/apex/plugins</code> or <code class="language-plaintext highlighter-rouge">~/.config/apex/plugins</code></li>
</ol>

<p>Each of those directories can hold Apex plugins in the usual format:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>.apex/
  plugins/
    my-plugin/
      plugin.yml
      whatever-script-you-like</code></pre></div></div>

<p>When Apex builds the plugin list, <strong>earlier locations win by id</strong>. If a plugin with id <code class="language-plaintext highlighter-rouge">footnotes-plus</code> exists in both <code class="language-plaintext highlighter-rouge">.apex/plugins</code> and your global config dir, the project version is the one that runs.</p>

<h3 id="no-op-shadowing-turning-off-plugins-per-project">No-op shadowing: turning off plugins per project</h3>

<p>That id-based precedence also gives you a neat trick: <strong>no-op shadowing</strong>.</p>

<p>If there&rsquo;s a global plugin you usually like, but you don&rsquo;t want it in a specific project, you can &ldquo;shadow&rdquo; it by dropping an empty or no-op plugin with the same id into <code class="language-plaintext highlighter-rouge">.apex/plugins</code>. For example:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>.apex/
  plugins/
    kbd/
      plugin.yml
      noop.sh</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">plugin.yml</code> might look like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">id</span><span class="pi">:</span> <span class="s">kbd</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">KBD Noop</span></code></pre></div></div>

<p>Because the project copy of <code class="language-plaintext highlighter-rouge">kbd</code> is discovered first, it <strong>shadows</strong> the global one. You can also do the same trick with purely declarative regex plugins: define a plugin with the same id that simply doesn&rsquo;t match anything meaningful, and the global behavior is effectively disabled for that project.</p>

<h3 id="--list-plugins-now-understands-projects"><code class="language-plaintext highlighter-rouge">--list-plugins</code> now understands projects</h3>

<p>To make this discoverable, <code class="language-plaintext highlighter-rouge">apex --list-plugins</code> was updated to use the <strong>same resolution rules</strong> as the runtime plugin loader.</p>

<p>When you run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--list-plugins</span></code></pre></div></div>

<p>you&rsquo;ll see:</p>

<ul>
  <li>An &ldquo;Installed Plugins&rdquo; section that includes plugins from:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">./.apex/plugins</code></li>
      <li><code class="language-plaintext highlighter-rouge">BASE/.apex/plugins</code> (if <code class="language-plaintext highlighter-rouge">--base-dir</code> was used)</li>
      <li><code class="language-plaintext highlighter-rouge">&lt;git-root&gt;/.apex/plugins</code></li>
      <li>global config plugins</li>
    </ul>
  </li>
  <li>An &ldquo;Available Plugins&rdquo; section from the remote directory, <strong>filtered</strong> so remote entries are hidden when you already have a plugin with the same id installed anywhere (project or global).</li>
</ul>

<p>If a project plugin shadows a global one, you&rsquo;ll only see the project entry in the installed list, and the remote listing won&rsquo;t try to &ldquo;helpfully&rdquo; re-offer the same id.</p>

<h3 id="project-level-config-in-apexconfigyml">Project-level config in <code class="language-plaintext highlighter-rouge">.apex/config.yml</code></h3>

<p>Plugins aren&rsquo;t the only thing that benefit from scoping. Apex&rsquo;s configuration system now has an explicit <strong>project layer</strong>, alongside the existing global and per-document metadata.</p>

<p>Config is now read from these places:</p>

<ol>
  <li><strong>Global config</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/apex/config.yml</code>, or</li>
      <li><code class="language-plaintext highlighter-rouge">~/.config/apex/config.yml</code></li>
    </ul>
  </li>
  <li><strong>Project config</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">./.apex/config.yml</code></li>
      <li><code class="language-plaintext highlighter-rouge">BASE/.apex/config.yml</code> when using <code class="language-plaintext highlighter-rouge">--base-dir BASE</code></li>
      <li><code class="language-plaintext highlighter-rouge">&lt;git-root&gt;/.apex/config.yml</code> when inside a Git work tree</li>
    </ul>
  </li>
  <li><strong>Explicit metadata file</strong>
    <ul>
      <li>Any file you pass with <code class="language-plaintext highlighter-rouge">--meta-file FILE</code></li>
    </ul>
  </li>
  <li><strong>Per-document metadata</strong>
    <ul>
      <li>YAML front matter, MultiMarkdown metadata, or Pandoc title blocks</li>
    </ul>
  </li>
  <li><strong>Command-line metadata and flags</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">--meta KEY=VALUE</code>, <code class="language-plaintext highlighter-rouge">--mode</code>, <code class="language-plaintext highlighter-rouge">--pretty</code>, <code class="language-plaintext highlighter-rouge">--no-tables</code>, and so on</li>
    </ul>
  </li>
</ol>

<p>The merge order matters:</p>

<ul>
  <li>Global <code class="language-plaintext highlighter-rouge">config.yml</code> (lowest file precedence)</li>
  <li>Project <code class="language-plaintext highlighter-rouge">.apex/config.yml</code></li>
  <li><code class="language-plaintext highlighter-rouge">--meta-file FILE</code></li>
  <li>Document metadata</li>
  <li><code class="language-plaintext highlighter-rouge">--meta</code> and CLI flags (highest precedence)</li>
</ul>

<p>So if you put this in your <strong>global</strong> config:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">mode</span><span class="pi">:</span> <span class="s">unified</span>
<span class="na">pretty</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">wikilinks</span><span class="pi">:</span> <span class="kc">false</span></code></pre></div></div>

<p>and then in your <strong>project</strong> <code class="language-plaintext highlighter-rouge">.apex/config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">wikilinks</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">indices</span><span class="pi">:</span> <span class="kc">true</span></code></pre></div></div>

<p>you&rsquo;ll end up with:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">mode: unified</code></li>
  <li><code class="language-plaintext highlighter-rouge">pretty: true</code></li>
  <li><code class="language-plaintext highlighter-rouge">wikilinks: true</code>   # project overrides global</li>
  <li><code class="language-plaintext highlighter-rouge">indices: true</code>     # project-only addition</li>
</ul>

<p>Any <code class="language-plaintext highlighter-rouge">--meta-file</code> you pass on the command line layers on top of both, and document/CLI overrides still win last.</p>

<h3 id="a-quick-example-project-layout">A quick example project layout</h3>

<p>Here&rsquo;s what a repo might look like with all of this wired up:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>my-book/
  .git/
  .apex/
    config.yml
    plugins/
      figures/
        plugin.yml
        figures.py
      kbd/
        plugin.yml
  chapters/
    01-intro.md
    02-deep-dive.md</code></pre></div></div>

<p>Running:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="nb">cd </span>my-book
apex chapters/01-intro.md <span class="nt">--plugins</span></code></pre></div></div>

<p>will:</p>

<ul>
  <li>Load config from:
    <ul>
      <li>global <code class="language-plaintext highlighter-rouge">config.yml</code> (if any),</li>
      <li>then <code class="language-plaintext highlighter-rouge">./.apex/config.yml</code>,</li>
      <li>then any <code class="language-plaintext highlighter-rouge">--meta-file</code> you pass,</li>
    </ul>
  </li>
  <li>Run plugins from:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">./.apex/plugins</code> first,</li>
      <li>then fall back to global plugins,</li>
    </ul>
  </li>
  <li>Apply per-document metadata and CLI overrides on top.</li>
</ul>

<p>You get per-repo behavior <strong>without</strong> having to constantly remember the right <code class="language-plaintext highlighter-rouge">--meta-file</code> or a long list of flags.</p>

<h3 id="a-quick-note-on-insert">A quick note on <code class="language-plaintext highlighter-rouge">++insert++</code></h3>

<p>While we were in here, another small but handy syntax has been added: <code class="language-plaintext highlighter-rouge">++insert++</code>.</p>

<p><code class="language-plaintext highlighter-rouge">++insert++</code> gives you a lightweight way to add an <code class="language-plaintext highlighter-rouge">&lt;ins&gt;text&lt;/ins&gt;</code> tag to your document. It&rsquo;s just a little shorter and easier than typing out the tags manually. I try not to add too much esoteric markup to the syntax, but I&rsquo;ve seen <code class="language-plaintext highlighter-rouge">++</code> a couple of places and thought it a worthwhil addition.</p>

<h3 id="wrapping-up">Wrapping up</h3>

<p>To recap:</p>

<ul>
  <li>Plugins can now live in <code class="language-plaintext highlighter-rouge">.apex/plugins</code> at the project level, and they override global plugins by id.</li>
  <li><code class="language-plaintext highlighter-rouge">--list-plugins</code> shows the <strong>actual</strong> set of plugins Apex will run for your current project, including overrides.</li>
  <li>Config can now live in <code class="language-plaintext highlighter-rouge">.apex/config.yml</code>, layered on top of your global <code class="language-plaintext highlighter-rouge">config.yml</code> and below any explicit <code class="language-plaintext highlighter-rouge">--meta-file</code>, document metadata, and flags.</li>
  <li><code class="language-plaintext highlighter-rouge">++insertion++</code> gives you <code class="language-plaintext highlighter-rouge">&lt;ins&gt;</code> tags.</li>
</ul>

<p>If you&rsquo;ve been juggling different shell aliases or wrapper scripts for each project, you can probably simplify a lot of that now by letting Apex&rsquo;s own project-aware behavior do the heavy lifting.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115974151781282518">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Project-level+plugins+and+config+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F28%2Fproject-level-plugins-and-config-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F28%2Fproject-level-plugins-and-config-for-apex%2F&text=Project-level+plugins+and+config+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F28%2Fproject-level-plugins-and-config-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17265388.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Dy.lan: turn your local network into a workflow engine]]></title>
    <link href="https://brett.trpstra.net/link/535/17264591/a-url-router-that-turns-your-local-network-into-a-workflow-engine"/>
    <updated>2026-01-27T08:00:00-06:00</updated>
    <published>2026-01-27T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/27/a-url-router-that-turns-your-local-network-into-a-workflow-engine</id>
    <content type="html"><![CDATA[
<p>Have you ever wished you could turn your local network into a smart routing system? Instead of remembering IP addresses and ports, what if you could use simple, memorable URLs that trigger workflows, redirect to services, or even search your notes?</p>

<p>That&rsquo;s exactly what <a href="https://github.com/rhsev/dy.lan">dy.lan</a> does. It&rsquo;s a self-hosted URL router with a plugin architecture that transforms your local network into a powerful automation platform. Built by Ralf Hülsmann, a Ruby rookie from Sevelen, Germany, dy.lan (pronounced &ldquo;Dylan&rdquo;) is a lightweight HTTP router designed specifically for local networks.</p>

<h2 id="the-basic-concept">The Basic Concept</h2>

<p>At its core, dy.lan acts as a central entry point that translates URLs into actions. Instead of remembering <code class="language-plaintext highlighter-rouge">192.168.1.73:8384</code> for Syncthing, you can access <code class="language-plaintext highlighter-rouge">http://sync.lan</code>. Instead of writing complex scripts, you can use <code class="language-plaintext highlighter-rouge">http://dy.lan/n/meeting</code> to search your Apple Notes.</p>

<p>The project solves several common problems:</p>

<ul>
  <li><strong>Infrastructure abstraction</strong>: When a service moves to a new IP or port, you update one config line instead of hunting down bookmarks and scripts</li>
  <li><strong>Workflow shortcuts</strong>: Turn URLs into actions with pattern-based routing</li>
  <li><strong>Clean local services</strong>: Route HTTP traffic without the complexity of full reverse proxies like Traefik or nginx for simple use cases</li>
  <li><strong>Extensibility</strong>: YAML configs for simple redirects, Ruby plugins for custom logic</li>
</ul>

<h2 id="plugin-architecture-and-extensibility">Plugin Architecture and Extensibility</h2>

<p>What makes dy.lan powerful is its plugin system. The architecture is built around numbered plugins (00-, 10-, 20-&hellip;) that follow a first-match-wins priority system. Each plugin can:</p>

<ul>
  <li>Match URLs using regex patterns with capture groups</li>
  <li>Filter by domain/host</li>
  <li>Execute custom Ruby logic</li>
  <li>Handle timeouts (default 500ms, configurable per plugin)</li>
  <li>Auto-disable after 5 errors (circuit breaker pattern)</li>
</ul>

<p>Plugins are hot-reloadable for YAML configs (after domain-match), and the system is resilient &mdash; syntax errors and loops won&rsquo;t crash the server.</p>

<p>The project includes 8 example plugins covering everything from simple redirects to API integrations, monitoring dashboards, and cron jobs. You can extend it with any feature you can implement in Ruby.</p>

<h2 id="real-world-examples">Real-World Examples</h2>

<p>Here are some practical ways dy.lan can be used:</p>

<p><strong>Google Search Shortcut</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c1"># config/redirects.yaml</span>
<span class="na">redirects</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">pattern</span><span class="pi">:</span> <span class="s1">'</span><span class="s">^/g/(.+)$'</span>
    <span class="na">target</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://google.com/search?q=${1}'</span></code></pre></div></div>

<p>Access <code class="language-plaintext highlighter-rouge">http://dy.lan/g/ruby</code> and it redirects to a Google search for &ldquo;ruby&rdquo;. Simple, memorable, and no coding required.</p>

<p><strong>DEVONthink Search</strong>
For more complex workflows, you can use a Ruby plugin:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="k">class</span> <span class="nc">DevonthinkPlugin</span> <span class="o">&lt;</span> <span class="no">Dylan</span><span class="o">::</span><span class="no">Plugin</span>
  <span class="n">pattern</span> <span class="sr">%r{^/(</span><span class="se">\d</span><span class="sr">{8})}</span>

  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">request</span><span class="p">)</span>
    <span class="n">alias_id</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="n">pattern</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
    <span class="no">Dylan</span><span class="o">::</span><span class="no">Response</span><span class="p">.</span><span class="nf">redirect</span><span class="p">(</span><span class="s2">"x-devonthink-item://</span><span class="si">#{</span><span class="n">alias_id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">http://dy.lan/12345678</code> opens the document with that alias in DEVONthink.</p>

<p><strong>Apple Notes Search</strong></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="k">class</span> <span class="nc">NotesPlugin</span> <span class="o">&lt;</span> <span class="no">Dylan</span><span class="o">::</span><span class="no">Plugin</span>
  <span class="n">pattern</span> <span class="sr">%r{^/n/(.+)}</span>

  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">request</span><span class="p">)</span>
    <span class="n">query</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="n">pattern</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
    <span class="no">Dylan</span><span class="o">::</span><span class="no">Response</span><span class="p">.</span><span class="nf">redirect</span><span class="p">(</span><span class="s2">"shortcuts://run-shortcut?name=search_notes&amp;input=</span><span class="si">#{</span><span class="n">query</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></div></div>

<p>Access <code class="language-plaintext highlighter-rouge">http://dy.lan/n/example</code> to search your Apple Notes for &ldquo;example&rdquo; via Shortcuts.</p>

<h2 id="deployment-options">Deployment Options</h2>

<p>One of the great things about dy.lan is its flexibility. It can run on your Mac for local development or on a Synology NAS for 24/7 operation.</p>

<p><strong>On Your Mac:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>git clone https://github.com/rhsev/dy.lan
<span class="nb">cd </span>dy.lan
docker-compose <span class="nt">-f</span> docker-compose.mac.yml up <span class="nt">-d</span></code></pre></div></div>

<p><strong>On Synology:</strong>
The project includes <code class="language-plaintext highlighter-rouge">docker-compose.synology.yml</code> for easy deployment on NAS devices. With macvlan networking, you can give dy.lan its own dedicated IP address, avoiding conflicts with other reverse proxies.</p>

<p>The performance is impressive: 6,000 requests per second on a Mac mini M4, and 2,500 requests per second on a Synology DS224+ (compared to ~200 req/s for YOURLS on similar hardware). All while using just 20-30 MB of RAM.</p>

<h2 id="technical-highlights">Technical Highlights</h2>

<p>Built with Ruby 4.0&rsquo;s async/fiber-based concurrency, dy.lan uses non-blocking I/O so slow plugins don&rsquo;t block fast ones. It&rsquo;s a pure Ruby implementation with no Rails, no middleware, and no database. Configuration is done through YAML files and Ruby plugins, with a browser-based dashboard for stats and container management.</p>

<p>The system includes modern Ruby syntax (using <code class="language-plaintext highlighter-rouge">it</code> parameter in core code while keeping plugins explicit), configurable timeouts, circuit breakers, and domain filtering per plugin. It&rsquo;s designed to be simple, transparent, and easy to extend.</p>

<h2 id="getting-started">Getting Started</h2>

<p>If you want to try dy.lan yourself, check out the <a href="https://github.com/rhsev/dy.lan">GitHub repository</a>. The project is shared as-is, built to solve specific friction in personal automation workflows. Ralf has been developing and sharing this with me for a while, and I&rsquo;m glad he decided to make it public so others can benefit.</p>

<p>Whether you need simple URL redirects, workflow automation, or a lightweight reverse proxy for local services, dy.lan provides a clean and extensible solution.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115968136522432127">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Dy.lan%3A+turn+your+local+network+into+a+workflow+engine%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F27%2Fa-url-router-that-turns-your-local-network-into-a-workflow-engine%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F27%2Fa-url-router-that-turns-your-local-network-into-a-workflow-engine%2F&text=Dy.lan%3A+turn+your+local+network+into+a+workflow+engine&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F27%2Fa-url-router-that-turns-your-local-network-into-a-workflow-engine%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17264591.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Tween: Fish function for outputting ranges of text]]></title>
    <link href="https://brett.trpstra.net/link/535/17260953/tween-fish-function-for-outputting-ranges-of-text"/>
    <updated>2026-01-22T10:51:00-06:00</updated>
    <published>2026-01-22T10:51:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/22/tween-fish-function-for-outputting-ranges-of-text</id>
    <content type="html"><![CDATA[
<p>I&rsquo;ve been developing a Fish function called <code class="language-plaintext highlighter-rouge">tween</code>. It&rsquo;s a simple, flexible utility for extracting ranges of lines from files or STDIN input, and it&rsquo;s flexible enough to handle just about any scenario you can throw at it, from numeric ranges, array style position/length ranges, or even string matching with regex capabilities.</p>

<p>You can find the source code in my <a href="https://github.com/ttscoff/fish_files">Fish functions repository</a>, specifically at <a href="https://github.com/ttscoff/fish_files/blob/master/functions/tween.fish">tween.fish</a>.</p>

<h2 id="what-it-does">What it does</h2>

<p><code class="language-plaintext highlighter-rouge">tween</code> displays lines between a start and end point. The start and end can be line numbers, string matches, or regex patterns. It supports multiple ranges, relative offsets, and even works with piped input.</p>

<h2 id="basic-usage">Basic usage</h2>

<p>The simplest form is specifying line numbers:</p>

<pre><code class="language-fish">tween file.txt 10 20
</code></pre>

<p>This displays lines 10 through 20 (inclusive). You can also use a dashed range format:</p>

<pre><code class="language-fish">tween file.txt 10-20
</code></pre>

<p>And here&rsquo;s the nice part: arguments can be in any order. Both of these work the same way:</p>

<pre><code class="language-fish">tween file.txt 10 20
tween 10 20 file.txt
</code></pre>

<p>If you only specify a single line number, <code class="language-plaintext highlighter-rouge">tween</code> will display from that line to the end of the file:</p>

<pre><code class="language-fish">tween file.txt 50
</code></pre>

<p>This shows lines 50 through the end of the file.</p>

<h2 id="multiple-ranges">Multiple ranges</h2>

<p>Need to extract several sections? Just separate them with commas:</p>

<pre><code class="language-fish">tween file.txt 10-20,30-40
tween file.txt 10 20, 30 40
</code></pre>

<p>Both formats work, so use whichever feels more natural.</p>

<h2 id="relative-offsets">Relative offsets</h2>

<p>Sometimes you know where you want to start but need to go a certain number of lines forward. Use <code class="language-plaintext highlighter-rouge">+N</code> for relative offsets:</p>

<pre><code class="language-fish">tween file.txt 10 +20
</code></pre>

<p>This displays lines 10 through 30 (10 plus 20 lines). You can also count from the end using <code class="language-plaintext highlighter-rouge">-N</code>:</p>

<pre><code class="language-fish">tween file.txt 50 -10
</code></pre>

<p>This shows lines 50 to 10 lines from the end of the file. There&rsquo;s a special case: <code class="language-plaintext highlighter-rouge">-1</code> means the end of the file (the last line):</p>

<pre><code class="language-fish">tween file.txt 50 -1
</code></pre>

<p>This displays lines 50 through the end of the file. Other negative numbers like <code class="language-plaintext highlighter-rouge">-2</code>, <code class="language-plaintext highlighter-rouge">-3</code>, etc. still mean &ldquo;N lines from the end&rdquo; (second-to-last, third-to-last, etc.).</p>

<h2 id="string-matching">String matching</h2>

<p>Instead of line numbers, you can match on strings. This is super useful when you know the content but not the exact line:</p>

<pre><code class="language-fish">tween file.txt 'START' +20
</code></pre>

<p>This finds the line containing &ldquo;START&rdquo; and displays it plus the next 20 lines. You can also use string matching for both start and end:</p>

<pre><code class="language-fish">tween file.txt 50-'END'
</code></pre>

<p>This shows lines 50 through the line containing &ldquo;END&rdquo;.</p>

<h2 id="regex-patterns">Regex patterns</h2>

<p>For more complex matching, use regex patterns wrapped in slashes:</p>

<pre><code class="language-fish">tween file.txt /START/ +20
</code></pre>

<p>This matches the regex pattern &ldquo;START&rdquo; and displays that line plus 20 more. You can also use the <code class="language-plaintext highlighter-rouge">-r</code> or <code class="language-plaintext highlighter-rouge">--regex</code> flag to treat all string arguments as regex:</p>

<pre><code class="language-fish">tween -r file.txt 'foo' 'bar'
</code></pre>

<p>Both patterns are treated as regex when using the <code class="language-plaintext highlighter-rouge">-r</code> flag.</p>

<h2 id="exclusive-mode">Exclusive mode</h2>

<p>Sometimes you want the lines <em>between</em> two markers but not the markers themselves. Use <code class="language-plaintext highlighter-rouge">-e</code> or <code class="language-plaintext highlighter-rouge">--exclusive</code>:</p>

<pre><code class="language-fish">tween -e file.txt 'BEGIN' 'END'
</code></pre>

<p>This displays all lines between (but not including) the lines containing &ldquo;BEGIN&rdquo; and &ldquo;END&rdquo;.</p>

<h2 id="syntax-highlighting-with-bat">Syntax highlighting with bat</h2>

<p>If you have <code class="language-plaintext highlighter-rouge">bat</code> installed, you can use it for syntax highlighting with the <code class="language-plaintext highlighter-rouge">-b</code> or <code class="language-plaintext highlighter-rouge">--bat</code> flag:</p>

<pre><code class="language-fish">tween -b file.txt 10-20
</code></pre>

<p>This displays the range with syntax highlighting, which is especially nice when viewing code.</p>

<h2 id="piped-input">Piped input</h2>

<p><code class="language-plaintext highlighter-rouge">tween</code> works with piped input too. Just use <code class="language-plaintext highlighter-rouge">-</code> as the file argument:</p>

<pre><code class="language-fish">cat file.txt | tween 10-20,30-40 -
</code></pre>

<p>This is handy when you&rsquo;re already working with a pipeline.</p>

<h2 id="options-summary">Options summary</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-e, --exclusive</code> - Exclude the start and end lines from output</li>
  <li><code class="language-plaintext highlighter-rouge">-b, --bat</code> - Use bat instead of sed for syntax highlighting</li>
  <li><code class="language-plaintext highlighter-rouge">-r, --regex</code> - Treat all string arguments as regular expressions</li>
  <li><code class="language-plaintext highlighter-rouge">-h, --help</code> - Show help message</li>
</ul>

<p>The function is flexible enough to handle most text extraction tasks, and the ability to mix line numbers, strings, and regex makes it incredibly versatile. Give it a try and see how it fits into your workflow!</p>


<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115939853563229265">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Tween%3A+Fish+function+for+outputting+ranges+of+text%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F22%2Ftween-fish-function-for-outputting-ranges-of-text%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F22%2Ftween-fish-function-for-outputting-ranges-of-text%2F&text=Tween%3A+Fish+function+for+outputting+ranges+of+text&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F22%2Ftween-fish-function-for-outputting-ranges-of-text%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17260953.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Markdown Fixup plugin for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17256661/markdown-fixup-plugin-for-apex"/>
    <updated>2026-01-19T12:13:00-06:00</updated>
    <published>2026-01-19T12:13:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/19/markdown-fixup-plugin-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/mdfixup-apex-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>My two biggest projects over the last week have been <a href="https://github.com/ttscoff/md-fixup">Markdown Fixup</a> and <a href="https://github.com/ApexMarkdown/apex">Apex</a>. It seemed worthwhile to integrate the two in some useful way.</p>

<blockquote>
  <p>A quick update on Markdown Fixup: I fixed the regex replacement engine to handle multi-line replacements. This is kind of a big deal if you&rsquo;re trying to convert things like BBCode to Markdown as part of the pipeline.</p>
</blockquote>

<p>I&rsquo;ve added a new plugin that integrates <code class="language-plaintext highlighter-rouge">md-fixup</code> directly into Apex&rsquo;s processing pipeline. If you haven&rsquo;t been tracking md-fixup, it&rsquo;s an opinionated markdown linter and fixer that can normalize spacing, fix emphasis markers, and apply custom regex replacements to your markdown files. And if you haven&rsquo;t been following Apex, it&rsquo;s my &ldquo;universal markdown processor&rdquo; project that handles all kinds of Markdown extensions in one place.</p>

<h2 id="why-a-plugin">Why a plugin?</h2>

<p>You could always just pipe md-fixup into apex:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>md-fixup file.md | apex <span class="nt">--mode</span> kramdown</code></pre></div></div>

<p>That works fine for simple cases, but if you want to run md-fixup as part of a pipeline with other Apex plugins, the plugin version is the way to go. Plugins run in a deterministic order, and you can enable or disable them as needed without changing your workflow.</p>

<h2 id="when-it-runs">When it runs</h2>

<p>The md-fixup plugin runs in the <code class="language-plaintext highlighter-rouge">pre_parse</code> phase, which means it processes your raw markdown text before Apex parses it. This is important for a couple of reasons:</p>

<ul>
  <li><strong>Compatibility</strong>: md-fixup can normalize your markdown to ensure it&rsquo;s compatible with Apex&rsquo;s formatting expectations</li>
  <li><strong>Regex replacements</strong>: Most importantly, if you&rsquo;re using custom regex replacements in your <code class="language-plaintext highlighter-rouge">replacements.yml</code> file, those run before Apex processes the text. This means you can transform markdown syntax itself, not just the final HTML output</li>
</ul>

<h2 id="installation">Installation</h2>

<p>The plugin is available in the Apex plugin directory. You can see it listed with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--list-plugins</span></code></pre></div></div>

<p>Install it with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--install-plugin</span> md-fixup</code></pre></div></div>

<p>The plugin assumes you already have the <a href="https://github.com/ttscoff/md-fixup?tab=readme-ov-file#installation">md-fixup binary installed</a> and available in your PATH. (And it uses the Rust version, which is what <code class="language-plaintext highlighter-rouge">brew install md-fixup</code> will install.)</p>

<h2 id="configuring-replacements">Configuring replacements</h2>

<p>After installation, the plugin creates a support directory at <code class="language-plaintext highlighter-rouge">~/.config/apex/support/md-fixup/</code> with a template <code class="language-plaintext highlighter-rouge">replacements.yml</code> file. Edit this file to add your custom regex replacements.</p>

<p>The file uses YAML format with a <code class="language-plaintext highlighter-rouge">replacements:</code> array. Each replacement has:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">name</code>: A descriptive name for the replacement</li>
  <li><code class="language-plaintext highlighter-rouge">pattern</code>: A regex pattern to match</li>
  <li><code class="language-plaintext highlighter-rouge">replacement</code>: The replacement text (can use regex groups like <code class="language-plaintext highlighter-rouge">$1</code>, <code class="language-plaintext highlighter-rouge">$2</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">timing</code>: When to apply (<code class="language-plaintext highlighter-rouge">before</code> or <code class="language-plaintext highlighter-rouge">after</code> other processing)</li>
  <li><code class="language-plaintext highlighter-rouge">in_code_blocks</code>: Whether to apply inside code blocks (<code class="language-plaintext highlighter-rouge">true</code>/<code class="language-plaintext highlighter-rouge">false</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">in_frontmatter</code>: Whether to apply in frontmatter (<code class="language-plaintext highlighter-rouge">true</code>/<code class="language-plaintext highlighter-rouge">false</code>)</li>
</ul>

<p>Here&rsquo;s an example:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">replacements</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">normalize-http"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">after</span>
    <span class="na">in_code_blocks</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="na">in_frontmatter</span><span class="pi">:</span> <span class="kc">false</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">fix-double-spaces"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="nv">  </span><span class="s">+"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">after</span>
    <span class="na">in_code_blocks</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="na">in_frontmatter</span><span class="pi">:</span> <span class="kc">false</span></code></pre></div></div>

<h2 id="running-with-plugins">Running with plugins</h2>

<p>To use the plugins on a file, you can enable it globally, or run them for a specific conversion:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--plugins</span> file.md</code></pre></div></div>

<p>The plugin runs automatically if you&rsquo;ve enabled plugins globally.</p>

<p>The plugin is hosted at <a href="https://github.com/ApexMarkdown/apex-plugin-md-fixup">https://github.com/ApexMarkdown/apex-plugin-md-fixup</a> if you want to check out the code or contribute improvements.</p>

<p>And if you haven&rsquo;t, <a href="https://github.com/ApexMarkdown/apex">check out Apex</a>.</p>

<p>Quick side note: it&rsquo;s been pointed out that md-fixup makes a great
Marked preprocessor. Marked 3 adds enough search and replace
capabilities that it might not be necessary, but for Marked 2, or if you
prefer to edit all your replacements in a dedicated YAML file, you can
just set <code class="language-plaintext highlighter-rouge">md-fixup</code> as your Custom Proprocessor.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115923183050762555">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Markdown+Fixup+plugin+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F19%2Fmarkdown-fixup-plugin-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F19%2Fmarkdown-fixup-plugin-for-apex%2F&text=Markdown+Fixup+plugin+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F19%2Fmarkdown-fixup-plugin-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17256661.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdfixup-apex-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdfixup-apex-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Markdown to Sendy with Image Stacks and File Uploads]]></title>
    <link href="https://brett.trpstra.net/link/535/17255313/markdown-to-sendy-with-image-stacks-and-file-uploads"/>
    <updated>2026-01-17T06:22:00-06:00</updated>
    <published>2026-01-17T06:22:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/17/markdown-to-sendy-with-image-stacks-and-file-uploads</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/mdtosendy-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve updated my Markdown to Sendy script with the ability to use &ldquo;sliced&rdquo; images with separate links, the ability to upload assets to a CDN automatically, and a &ldquo;test email&rdquo; mode that will actually send a test email to you without going through Sendy.</p>

<p>If you haven&rsquo;t heard of <a href="https://github.com/ttscoff/mdtosendy">mdtosendy</a> before, it&rsquo;s a Ruby script that converts Markdown files into email-ready HTML with automatic inline styling for maximum email client compatibility. While it can generate emails for any newsletter or email platform, it integrates best with Sendy, automatically creating and scheduling campaigns. You write your emails in Markdown, maintain your styles in CSS files, and mdtosendy handles all the messy email HTML details. And it can handle multiple templates, so you can have different styles for the same newsletter, and/or configure multiple instances with different styles/settings.</p>

<p>The latest version adds some powerful new features that make it even more useful for email workflows.</p>

<h3 id="test-email-sending">Test Email Sending</h3>

<p>The HTML preview mode is useful, but nothing beats being able to actually view the email in an email app so you can see exactly how it will render. Now you can send test emails directly without going through Sendy. The new <code class="language-plaintext highlighter-rouge">--test-send</code> option does exactly that:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>mdtosendy <span class="nt">--test-send</span> your-email@example.com your-email.md</code></pre></div></div>

<p>This sends a test email directly via SMTP, bypassing Sendy entirely. Perfect for quick previews or testing your email design before creating a campaign. You&rsquo;ll need to configure SMTP settings in your <code class="language-plaintext highlighter-rouge">config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">smtp</span><span class="pi">:</span>
  <span class="na">host</span><span class="pi">:</span> <span class="s2">"</span><span class="s">smtp.gmail.com"</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">587</span>
  <span class="na">domain</span><span class="pi">:</span> <span class="s2">"</span><span class="s">gmail.com"</span>
  <span class="na">user</span><span class="pi">:</span> <span class="s2">"</span><span class="s">me@example.com"</span>
  <span class="na">password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-app-password-here"</span>
  <span class="na">auth</span><span class="pi">:</span> <span class="s2">"</span><span class="s">plain"</span>
  <span class="na">starttls</span><span class="pi">:</span> <span class="kc">true</span></code></pre></div></div>

<p>For Gmail, you&rsquo;ll need to generate an App Password (not your regular password) at https://myaccount.google.com/apppasswords. Most other SMTP providers follow a similar pattern.</p>

<h3 id="cdn-image-upload-with-overwrite-control">CDN Image Upload with Overwrite Control</h3>

<p>The CDN image upload feature is a significant enhancement. You can add a <code class="language-plaintext highlighter-rouge">cdn</code> section to config (which can be inherited from the main config, or set per template), and when <code class="language-plaintext highlighter-rouge">mdtosendy</code> detects a local file path, it will be uploaded to the CDN and the url in the output HTML will be updated accordingly.</p>

<p>CDN uploads work with S3, SCP, or SFTP. See the example config in the repo for details.</p>

<p>You can configure how <code class="language-plaintext highlighter-rouge">mdtosendy</code> handles existing files on your CDN with the <code class="language-plaintext highlighter-rouge">overwrite</code> option:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">cdn</span><span class="pi">:</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://cdn.example.com"</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">s3"</span>
  <span class="na">username</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-access-key-id"</span>
  <span class="na">password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-secret-access-key"</span>
  <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-bucket-name"</span>
  <span class="na">overwrite</span><span class="pi">:</span> <span class="s">ask</span>  <span class="c1"># Options: true, false, or ask/prompt</span></code></pre></div></div>

<p>By default, mdtosendy uses original filenames (no timestamps) and prompts you when a file already exists. The prompt defaults to &ldquo;Yes&rdquo;, so you can just press Enter or type <code class="language-plaintext highlighter-rouge">y</code> to overwrite, or <code class="language-plaintext highlighter-rouge">n</code> to skip. If you choose not to overwrite, a timestamp will be added to the filename to avoid conflicts.</p>

<p>The <code class="language-plaintext highlighter-rouge">overwrite</code> setting supports three modes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">true</code> - Always overwrite without prompting</li>
  <li><code class="language-plaintext highlighter-rouge">false</code> - Never overwrite, automatically add timestamps when files exist</li>
  <li><code class="language-plaintext highlighter-rouge">ask</code> or <code class="language-plaintext highlighter-rouge">prompt</code> - Prompt for confirmation (default behavior)</li>
</ul>

<p>This works with all three CDN types: S3, SCP, and SFTP. The file existence checking is smart enough to detect existing files before uploading.</p>

<h3 id="image-stack-tag">Image Stack Tag</h3>

<p>The new <code class="language-plaintext highlighter-rouge">{% stack %}</code> tag is perfect for creating multi-part images that need to be displayed as a seamless vertical stack. This is especially useful for sliced images or creating visual sections in your emails.</p>

<p>You can use it with markdown image syntax:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% stack %}
<span class="p">[</span><span class="nv">![</span><span class="p">](</span><span class="sx">/images/image1.png</span><span class="p">)</span>](https://example.com/link1)
<span class="p">[</span><span class="nv">![</span><span class="p">](</span><span class="sx">/images/image2.png</span><span class="p">)</span>](https://example.com/link2)
<span class="p">[</span><span class="nv">![</span><span class="p">](</span><span class="sx">/images/image3.png</span><span class="p">)</span>](https://example.com/link3)
{% endstack %}</code></pre></div></div>

<p>Or with YAML for more structured definitions:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% stack type="yaml" %}
images:
<span class="p">  -</span> path: /images/image1.png
    url: https://example.com/link1
<span class="p">  -</span> path: /images/image2.png
    url: https://example.com/link2
<span class="p">  -</span> path: /images/image3.png
{% endstack %}</code></pre></div></div>

<p>The stack tag creates a vertical stack of images with zero spacing between them, all full width of the content area. Each image can optionally have a link. Local images in stacks are automatically uploaded to your CDN if you have it configured, and the tag uses a table-based layout for maximum email client compatibility.</p>

<h3 id="demo-please">Demo Please!</h3>

<p>Here&rsquo;s an email generated from the <code class="language-plaintext highlighter-rouge">demo.md</code> file in the repository,
using my own <code class="language-plaintext highlighter-rouge">marked</code> template, which uploads to my cdn.marked2app.com
bucket on S3. The demo shows a single image uploaded to the CDN from a
local file path, as well as 3 separate images combined into one stack,
with 3 separate links.</p>

<p><a href="https://stuff.brettterpstra.com/demo-2.html" class="btn--big" target="_blank">Demo email</a></p>

<h3 id="getting-started">Getting Started</h3>

<p>If you&rsquo;re new to <code class="language-plaintext highlighter-rouge">mdtosendy</code>, installation is a one-liner:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>curl <span class="nt">-s</span> https://github.com/ttscoff/mdtosendy/raw/main/bootstrap.sh | bash</code></pre></div></div>

<p>For more details on all the features, configuration options, and examples, check out the <a href="https://github.com/ttscoff/mdtosendy">mdtosendy repository on GitHub</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115910698917028512">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Markdown+to+Sendy+with+Image+Stacks+and+File+Uploads%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F17%2Fmarkdown-to-sendy-with-image-stacks-and-file-uploads%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F17%2Fmarkdown-to-sendy-with-image-stacks-and-file-uploads%2F&text=Markdown+to+Sendy+with+Image+Stacks+and+File+Uploads&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F17%2Fmarkdown-to-sendy-with-image-stacks-and-file-uploads%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17255313.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdtosendy-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdtosendy-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Marked features for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17254761/marked-features-for-apex"/>
    <updated>2026-01-16T08:00:00-06:00</updated>
    <published>2026-01-16T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/16/marked-features-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>When <a href="https://github.com/ApexMarkdown/apex">Apex</a> reaches 1.0, I&rsquo;m planning to include it in Marked 3. I realized that Marked has a lot of preprocessing
features that were previously handled in Objective-C that
would make sense to have in the core processor for both
speed and accessibility from the command line.</p>

<p>So I&rsquo;ve added a bunch of new flags and C API definitions to
Apex that bring some of Marked&rsquo;s capabilities directly into
the processor. These are all available via command-line
flags, configuration options, and the C API.</p>

<h2 id="hashtags">Hashtags</h2>

<p>The <code class="language-plaintext highlighter-rouge">--hashtags</code> flag converts <code class="language-plaintext highlighter-rouge">#tags</code> in your Markdown into
span-wrapped hashtags. By default, it uses the <code class="language-plaintext highlighter-rouge">mkhashtag</code>
class, but you can use <code class="language-plaintext highlighter-rouge">--style-hashtags</code> to use
<code class="language-plaintext highlighter-rouge">mkstyledtag</code> instead.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex document.md <span class="nt">--hashtags</span>
apex document.md <span class="nt">--hashtags</span> <span class="nt">--style-hashtags</span></code></pre></div></div>

<p>This is smart enough to skip hashtags inside code blocks and
HTML attributes, so you won&rsquo;t get false matches in things
like <code class="language-plaintext highlighter-rouge">href="https://brettterpstra.com#anchor"</code> or code examples.</p>

<p>Hashtags are disabled by default because they would conflict
with headers if you&rsquo;re not in the habit of putting a space
after the <code class="language-plaintext highlighter-rouge">#</code> in an ATX header (e.g. <code class="language-plaintext highlighter-rouge">#Header 1</code>). This
option is only for people who want to convert things like
Bear notes with tag formatting. They can be enabled with
<code class="language-plaintext highlighter-rouge">--hashtags</code> on the command line, by including <code class="language-plaintext highlighter-rouge">hashtags: true</code>
in a config file, or by using the <code class="language-plaintext highlighter-rouge">enable_hashtags</code>
boolean when using the C API.</p>

<h2 id="random-footnote-ids">Random Footnote IDs</h2>

<p>When you&rsquo;re combining multiple documents, footnote ID
collisions can be a problem. The <code class="language-plaintext highlighter-rouge">--random-footnote-ids</code>
flag generates hash-based footnote IDs using an 8-character
hex prefix from the document content.</p>

<p>Instead of <code class="language-plaintext highlighter-rouge">fn-1</code> and <code class="language-plaintext highlighter-rouge">fnref-1</code>, you&rsquo;ll get <code class="language-plaintext highlighter-rouge">fn-a7b3c9d2-1</code>
and <code class="language-plaintext highlighter-rouge">fnref-a7b3c9d2-1</code>. Different documents get different
hash prefixes, so you can safely combine them without
conflicts.</p>

<h2 id="widont-for-headings">Widon&rsquo;t for Headings</h2>

<p>The <code class="language-plaintext highlighter-rouge">--widont</code> flag prevents short widows in headings by
inserting non-breaking spaces between trailing words. It
works backwards from the end of the heading, combining words
until the trailing portion exceeds 10 characters.</p>

<p>So a heading like &ldquo;introduction to the topic&rdquo; becomes
<code class="language-plaintext highlighter-rouge">introduction to&amp;nbsp;the&amp;nbsp;topic</code> &mdash; ensuring that if
the heading wraps, the trailing portion won&rsquo;t be a short,
lonely word on its own line.</p>

<p>Widon&rsquo;t is disabled by default in all modes, as it might
create potentially unexpected results if the user isn&rsquo;t
aware of it. It can be explicitly enabld with <code class="language-plaintext highlighter-rouge">--widont</code>,
<code class="language-plaintext highlighter-rouge">widont: true</code> in config, or with the <code class="language-plaintext highlighter-rouge">enable_widont</code>
boolean in the C API.</p>

<h2 id="code-is-poetry">Code is Poetry</h2>

<p>Code blocks without a programming language specified can be
treated as poetry with the <code class="language-plaintext highlighter-rouge">--code-is-poetry</code> flag. This
adds a <code class="language-plaintext highlighter-rouge">poetry</code> class to code blocks that don&rsquo;t have a
language specified, and automatically enables
<code class="language-plaintext highlighter-rouge">--highlight-language-only</code> so only code blocks with
languages get syntax highlighting.</p>

<p>Works for both fenced code blocks and indented code blocks.</p>

<p>Again, this is disabled by default as it has very specific
use cases.</p>

<h2 id="proofreader-mode">Proofreader Mode</h2>

<p>The <code class="language-plaintext highlighter-rouge">--proofreader</code> flag converts <code class="language-plaintext highlighter-rouge">==highlight==</code> and
<code class="language-plaintext highlighter-rouge">~~delete~~</code> syntax into CriticMarkup highlight and
deletion. It automatically enables CriticMarkup processing,
so you can use this simpler syntax and still get the full
CriticMarkup rendering.</p>

<h2 id="markdown-in-html-toggle">Markdown in HTML Toggle</h2>

<p>Marked has always processed markdown inside HTML blocks with
<code class="language-plaintext highlighter-rouge">markdown</code> attributes. Now you can control this behavior
with <code class="language-plaintext highlighter-rouge">--markdown-in-html</code> and <code class="language-plaintext highlighter-rouge">--no-markdown-in-html</code>. It&rsquo;s
enabled by default in unified mode, but you can toggle it
for other modes or when you need stricter behavior.</p>

<h2 id="page-breaks">Page Breaks</h2>

<p>Two new flags for page break handling:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">--hr-page-break</code> replaces <code class="language-plaintext highlighter-rouge">&lt;hr&gt;</code> elements with
Marked-style page break divs</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">--page-break-before-footnotes</code> inserts a page break
before the footnotes section</p>
  </li>
</ul>

<p>Both use Marked&rsquo;s standard page break format with proper
attributes for styling and identification.</p>

<h2 id="title-from-h1">Title from H1</h2>

<p>When using <code class="language-plaintext highlighter-rouge">--standalone</code>, the <code class="language-plaintext highlighter-rouge">--title-from-h1</code> flag
extracts the text from the first H1 heading and uses it as
the document title if no title is specified via <code class="language-plaintext highlighter-rouge">--title</code> or
metadata. The H1 stays in the document body, but now it&rsquo;s
also in the <code class="language-plaintext highlighter-rouge">&lt;title&gt;</code> tag.</p>

<h2 id="all-available-everywhere">All Available Everywhere</h2>

<p>All of these features are available through:</p>

<ul>
  <li>Command-line flags (as shown above)</li>
  <li>Configuration files (<code class="language-plaintext highlighter-rouge">config.yml</code>, <code class="language-plaintext highlighter-rouge">--meta-file</code>)</li>
  <li>
    <p>Document metadata (YAML front matter, MultiMarkdown
metadata)</p>
  </li>
  <li>The C API (via the <code class="language-plaintext highlighter-rouge">apex_options</code> struct)</li>
</ul>

<p>This means you can set defaults in your config file,
override per-document in metadata, or use command-line flags
for one-off processing. The flexibility is there.</p>

<h2 id="why-build-these-into-apex">Why Build These Into Apex?</h2>

<p>These features were originally preprocessing steps in
Marked&rsquo;s Objective-C code. Moving them into Apex provides:</p>

<ol>
  <li>
    <p><strong>Speed</strong> &mdash; C-based processing is faster than
Objective-C string manipulation</p>
  </li>
  <li>
    <p><strong>Accessibility</strong> &mdash; Available from the command line
without needing Marked</p>
  </li>
  <li>
    <p><strong>Consistency</strong> &mdash; Same behavior whether called from
Marked or directly</p>
  </li>
  <li>
    <p><strong>Extensibility</strong> &mdash; Other tools can use these features
via the C API</p>
  </li>
</ol>

<p>As Apex approaches 1.0, these features will make the
integration with Marked a little easier while also making
some of Marked&rsquo;s capabilities available to anyone using Apex
directly. I know none of these recent changes are killer features for most people, so this is just serving as documentation of the development process.</p>

<p>As always, as Apex develops, I&rsquo;m very interested in what features you&rsquo;d like to see. The goal is to have one universal processor that, at least for HTML output<sup id="fnref:pandoc"><a href="https://brettterpstra.com#fn:pandoc" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, can do everything that other Markdown flavors can do. If you have ideas or requests, or want to contribute to the development, please <a href="https://github.com/ApexMarkdown/apex">join me on GitHub</a>!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:pandoc">
      <p>I&rsquo;m not going to try to replicate all of Pandoc&rsquo;s powerful conversion features, as you can just pipe Apex HTML output into Pandoc for easy conversion to PDF, DOCX, etc. I&rsquo;m just focusing on making as many extensions for HTML output as possible work.&nbsp;<a href="https://brettterpstra.com#fnref:pandoc" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115905169266877317">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Marked+features+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F16%2Fmarked-features-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F16%2Fmarked-features-for-apex%2F&text=Marked+features+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F16%2Fmarked-features-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17254761.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Git-based changelogs for Rust projects and more]]></title>
    <link href="https://brett.trpstra.net/link/535/17253954/git-based-changelogs-for-rust-projects-and-more"/>
    <updated>2026-01-15T08:00:00-06:00</updated>
    <published>2026-01-15T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/15/git-based-changelogs-for-rust-projects-and-more</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/changelog-header-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve been using my <a href="https://github.com/ttscoff/changelog">changelog</a> script for years to generate release notes from git commit messages. It&rsquo;s saved me countless hours and helped me maintain complete, informative changelogs across all my projects.</p>

<p>I wrote <code class="language-plaintext highlighter-rouge">changelog</code> <a href="https://brettterpstra.com/2017/08/14/automatic-release-notes-from-git-commit-messages/">in 2017</a> and mentioned it <a href="https://brettterpstra.com/2024/08/31/updated-generate-slick-changelogs-from-git-commits/">back in 2024</a>. I&rsquo;ve used it for every project I&rsquo;ve worked on since then, and I&rsquo;ve been improving it and making it work with more and more project types over the years.</p>

<h3 id="rust-project-support">Rust project support</h3>

<p>The latest update adds support for Rust projects, which I&rsquo;m just dipping my toes into. The script now automatically detects Rust projects in two ways:</p>

<ol>
  <li>
    <p><strong>Cargo.toml detection</strong>: If your project has a <code class="language-plaintext highlighter-rouge">Cargo.toml</code> file, it reads the <code class="language-plaintext highlighter-rouge">name</code> and <code class="language-plaintext highlighter-rouge">version</code> fields directly from it.</p>
  </li>
  <li>
    <p><strong>Rust source file scanning</strong>: For projects without a <code class="language-plaintext highlighter-rouge">Cargo.toml</code> (or as a fallback), it scans <code class="language-plaintext highlighter-rouge">.rs</code> files looking for version constants like <code class="language-plaintext highlighter-rouge">const VERSION: &amp;str = "1.0.0"</code> and command names from <code class="language-plaintext highlighter-rouge">Command::new("appname")</code>.</p>
  </li>
</ol>

<p>This means you can use the changelog script with any Rust project just by running <code class="language-plaintext highlighter-rouge">changelog</code> or <code class="language-plaintext highlighter-rouge">changelog -u</code> to update your <code class="language-plaintext highlighter-rouge">CHANGELOG.md</code> file, and it will automatically pull the version from your <code class="language-plaintext highlighter-rouge">Cargo.toml</code> or source files.</p>

<h3 id="other-improvements">Other improvements</h3>

<p>Along with Rust support, I&rsquo;ve made a few other improvements:</p>

<ul>
  <li>
    <p><strong>VERSION constant</strong>: The script now includes its own <code class="language-plaintext highlighter-rouge">VERSION</code> constant that stays in sync with the <code class="language-plaintext highlighter-rouge">VERSION</code> file, making it easier to track the script&rsquo;s version.</p>
  </li>
  <li>
    <p><strong>Code refactoring</strong>: I&rsquo;ve extracted the version detection, git log parsing, and formatting logic into separate modules (<code class="language-plaintext highlighter-rouge">VersionDetector</code>, <code class="language-plaintext highlighter-rouge">GitLogParser</code>, and <code class="language-plaintext highlighter-rouge">ChangelogFormatter</code>) to make the codebase more maintainable.</p>
  </li>
  <li>
    <p><strong>Better version detection</strong>: The script now has a more robust fallback chain for detecting versions, with Rust projects taking priority when detected, followed by Ruby gems, Xcode projects, and plain <code class="language-plaintext highlighter-rouge">VERSION</code> files.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--since-version</code> flag</strong>: You can now generate changelogs starting from a specific version tag using <code class="language-plaintext highlighter-rouge">--since-version</code> (or <code class="language-plaintext highlighter-rouge">--sv</code> for short). This is useful when you want to see changes since a particular release, and it supports partial version matching. For example, <code class="language-plaintext highlighter-rouge">changelog --since-version 1.0</code> will find the most recent tag starting with &ldquo;1.0&rdquo; and generate a changelog from that point.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--select</code> flag</strong>: If you prefer an interactive approach, use <code class="language-plaintext highlighter-rouge">--select</code> (or <code class="language-plaintext highlighter-rouge">-s</code>) to pop up an <code class="language-plaintext highlighter-rouge">fzf</code> menu of all your git tags. This lets you visually choose which tag to use as the starting point for your changelog generation. Perfect for when you can&rsquo;t remember the exact version number but know roughly when you want to start from.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--only</code> flag</strong>: Filter changelog output to show only specific change types. Use it like <code class="language-plaintext highlighter-rouge">changelog --only new,fixed</code> to see only new features and bug fixes, or <code class="language-plaintext highlighter-rouge">changelog --only changed</code> to see only breaking changes and modifications.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--split</code> flag</strong>: When used with <code class="language-plaintext highlighter-rouge">--select</code> or <code class="language-plaintext highlighter-rouge">--since-version</code>, this splits the changelog output by version tag, showing what changed in each release separately. This is great for generating comprehensive release notes that span multiple versions. You can control the order with <code class="language-plaintext highlighter-rouge">--order asc</code> (oldest first) or <code class="language-plaintext highlighter-rouge">--order desc</code> (newest first, default).</p>
  </li>
</ul>

<h3 id="how-it-works">How it works</h3>

<p>The script works exactly the same way it always has &mdash; you format your commit messages with prefixes like <code class="language-plaintext highlighter-rouge">NEW:</code> or <code class="language-plaintext highlighter-rouge">FIXED:</code>, or tags like <code class="language-plaintext highlighter-rouge">@new</code>, <code class="language-plaintext highlighter-rouge">@fixed</code>, <code class="language-plaintext highlighter-rouge">@changed</code>, etc., and it generates a changelog from commits since your last git tag. The only difference now is that it works seamlessly with Rust projects without any additional configuration.</p>

<p>The script is still designed to fit my personal workflow (including some nvUltra, Marked, and Bunch-specific formatting), but it&rsquo;s open for anyone to hack on and adapt to their needs. All of my project-specific paths are gated by exact directory structures and won&rsquo;t affect operation by anyone else.</p>

<p>If you&rsquo;re working with a Rust, Xcode, Ruby, or any script project and want to automate your changelog generation, <a href="https://github.com/ttscoff/changelog">give it a try</a>. It makes it so easy to build a changelog and release notes as you develop, and have them ready to go when you hit release time.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115899507184402063">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Git-based+changelogs+for+Rust+projects+and+more%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F15%2Fgit-based-changelogs-for-rust-projects-and-more%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F15%2Fgit-based-changelogs-for-rust-projects-and-more%2F&text=Git-based+changelogs+for+Rust+projects+and+more&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F15%2Fgit-based-changelogs-for-rust-projects-and-more%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17253954.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/changelog-header-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/changelog-header-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Regex Replacements for Markdown Fixup]]></title>
    <link href="https://brett.trpstra.net/link/535/17253235/regex-replacements-for-markdown-fixup"/>
    <updated>2026-01-14T08:00:00-06:00</updated>
    <published>2026-01-14T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/14/regex-replacements-for-markdown-fixup</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p><a href="https://www.barebones.com/products/bbedit">BBEdit</a> has a cool feature called
<a href="https://www.barebones.com/products/bbedit/benefitsexercise.html">Text Factories</a>
for automating repetitive text transformations. When
Younghart
<a href="https://forum.brettterpstra.com/t/a-quick-markdown-fixup-update/4418/2">mentioned it on the forum</a>,
it got me thinking.</p>

<p>While I can&rsquo;t replicate the full power of Text Factories
(which can chain multiple transformations, with all of
BBEdit&rsquo;s power), I could at least add a flexible regex
search-and-replace system to md-fixup.</p>

<p>So that&rsquo;s what I built: a YAML-driven regex replacement
engine that lets you define custom patterns that run as part
of an <a href="https://github.com/ttscoff/md-fixup"><code class="language-plaintext highlighter-rouge">md-fixup</code></a> pass. It&rsquo;s not a replacement for Text
Factories &mdash; it only does regex search and replace &mdash; but
it offers a way to extend <code class="language-plaintext highlighter-rouge">md-fixup</code> with your own
transformations.</p>

<h2 id="how-it-works">How It Works</h2>

<p>Replacements are defined in a YAML file and can be scoped to
run before or after Markdown Fixup&rsquo;s built-in rules. Each
replacement can optionally run inside code blocks or YAML
frontmatter, giving you fine-grained control over where
transformations happen.</p>

<p>The replacement file lives in one of these locations
(checked in order):</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">.md-fixup-replacements</code> in your current directory</li>
  <li>
    <p>The path specified in your config file&rsquo;s
<code class="language-plaintext highlighter-rouge">replacements_file:</code> key</p>
  </li>
  <li><code class="language-plaintext highlighter-rouge">~/.config/md-fixup/replacements.yml</code> (or
<code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/md-fixup/replacements.yml</code>)</li>
</ul>

<h2 id="setting-up-replacements">Setting Up Replacements</h2>

<p>Here&rsquo;s a simple example that fixes double spaces and
normalizes HTTP links:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">replacements</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">fix-double-spaces"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="nv">  </span><span class="s">+"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">after</span>
    <span class="na">in_code_blocks</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="na">in_frontmatter</span><span class="pi">:</span> <span class="kc">false</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">normalize-http"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">after</span></code></pre></div></div>

<p>Each replacement has a few key properties:</p>

<ul>
  <li>
    <p><strong>name</strong>: A human-readable identifier (useful for
debugging)</p>
  </li>
  <li>
    <p><strong>pattern</strong>: A Rust <code class="language-plaintext highlighter-rouge">regex</code> pattern (supports capture
groups)</p>
  </li>
  <li>
    <p><strong>replacement</strong>: The replacement string (use <code class="language-plaintext highlighter-rouge">$1</code>, <code class="language-plaintext highlighter-rouge">$2</code>,
etc. for capture groups)</p>
  </li>
  <li>
    <p><strong>timing</strong>: When to run&mdash;<code class="language-plaintext highlighter-rouge">before</code> or <code class="language-plaintext highlighter-rouge">after</code> the built-in
rules</p>
  </li>
  <li>
    <p><strong>in_code_blocks</strong>: If <code class="language-plaintext highlighter-rouge">true</code>, the pattern runs inside
fenced code blocks</p>
  </li>
  <li>
    <p><strong>in_frontmatter</strong>: If <code class="language-plaintext highlighter-rouge">true</code>, the pattern runs inside
YAML frontmatter</p>
  </li>
</ul>

<h2 id="a-more-complex-example">A More Complex Example</h2>

<p>Here&rsquo;s one that swaps version numbers using capture groups:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">replacements</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">swap-version"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="s">(</span><span class="se">\\</span><span class="s">d+)</span><span class="se">\\</span><span class="s">.(</span><span class="se">\\</span><span class="s">d+)"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$2.$1"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">before</span></code></pre></div></div>

<p>This would turn &ldquo;Version 1.2 is released&rdquo; into &ldquo;Version 2.1
is released&rdquo;. The pattern captures two groups of digits
separated by a dot, then swaps them in the replacement.
Stupid example, of course (why would you ever do that?), but
hopefully you get the idea.</p>

<h2 id="controlling-replacements">Controlling Replacements</h2>

<p>You can enable or disable replacements via your config file:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">width</span><span class="pi">:</span> <span class="m">80</span>
<span class="na">replacements</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">replacements_file</span><span class="pi">:</span> <span class="s">~/my-replacements.yml</span>
<span class="na">rules</span><span class="pi">:</span>
  <span class="na">skip</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">wrap</span></code></pre></div></div>

<p>Or use command-line flags:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Force enable</span>
md-fixup <span class="nt">--replacements</span> file.md

<span class="c"># Force disable</span>
md-fixup <span class="nt">--no-replacements</span> file.md

<span class="c"># Use a specific file</span>
md-fixup <span class="nt">--replacement-file</span> ./custom.yml file.md</code></pre></div></div>

<p>This regex replacement system is intentionally limited, and
as mentioned, is no replacement for Text Factories. It only
does regex search and replace, but it runs automatically as
part of md-fixup&rsquo;s fixup pass, which means you can integrate
custom transformations into your existing workflow without
switching tools.</p>

<p>If you need the full power of Text Factories, by all means
use <a href="https://www.barebones.com/products/bbedit">BBEdit</a>. It&rsquo;s an amazing text editor. But if you just
need a few regex replacements to run alongside Markdown
Fixup&rsquo;s built-in rules, this might be exactly what you&rsquo;re
looking for.</p>

<p>Check out the latest version of <a href="https://github.com/ttscoff/md-fixup">Markdown Fixup on GitHub</a>.
Find installation and usage instructions there!</p>


<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115893841894623108">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Regex+Replacements+for+Markdown+Fixup%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F14%2Fregex-replacements-for-markdown-fixup%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F14%2Fregex-replacements-for-markdown-fixup%2F&text=Regex+Replacements+for+Markdown+Fixup&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F14%2Fregex-replacements-for-markdown-fixup%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17253235.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Built-in Syntax Highlighting for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17252486/built-in-syntax-highlighting-for-apex"/>
    <updated>2026-01-13T08:42:00-06:00</updated>
    <published>2026-01-13T08:42:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/13/built-in-syntax-highlighting-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I think a lot of people using Apex are going to want syntax
highlighting of code blocks. Including a script like
Highlight.js in your HTML output is <em>fine</em>, but I wanted
Apex to be able to directly output HTML with the necessary
spans and tables for highlighting. So, introducing the
<code class="language-plaintext highlighter-rouge">--code-highlight</code> flag.</p>

<h3 id="how-it-works">How It Works</h3>

<p>Rather than bundling a syntax highlighting engine (which
would bloat the binary and require constant updates for new
languages), I decided to leverage external tools that you
probably already have installed.</p>

<p>The new <code class="language-plaintext highlighter-rouge">--code-highlight</code> flag accepts either <code class="language-plaintext highlighter-rouge">pygments</code> or
<code class="language-plaintext highlighter-rouge">skylighting</code> as arguments:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Using Pygments (Python-based)</span>
apex <span class="nt">--code-highlight</span> pygments input.md

<span class="c"># Using Skylighting (Haskell-based, used by Pandoc)</span>
apex <span class="nt">--code-highlight</span> skylighting input.md

<span class="c"># Short forms work too</span>
apex <span class="nt">--code-highlight</span> p input.md   <span class="c"># pygments</span>
apex <span class="nt">--code-highlight</span> s input.md   <span class="c"># skylighting</span></code></pre></div></div>

<p>When you specify a highlighter, Apex processes your Markdown
normally, then makes a second pass over the HTML output. It
finds all <code class="language-plaintext highlighter-rouge">&lt;pre&gt;&lt;code&gt;</code> blocks, extracts the raw code
content, pipes it through the external tool, and replaces
the original block with the colorized HTML output.</p>

<h3 id="language-detection">Language Detection</h3>

<p>Apex handles language specification in the standard way
you&rsquo;d expect. Just add the language identifier after the
opening fence:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">```</span><span class="nl">python
</span>
<span class="k">def</span> <span class="nf">hello</span><span class="p">():</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Hello, world!</span><span class="sh">"</span><span class="p">)</span>

<span class="p">```</span></code></pre></div></div>

<p>The language is extracted from either the
<code class="language-plaintext highlighter-rouge">class="language-XXX"</code> attribute on the code tag or the
<code class="language-plaintext highlighter-rouge">lang="XXX"</code> attribute on the pre tag (depending on how
cmark-gfm formatted it). This language is then passed to the
external highlighter.</p>

<p>If you don&rsquo;t specify a language, both Pygments and
Skylighting will attempt auto-detection. Pygments is
particularly good at this thanks to its <code class="language-plaintext highlighter-rouge">-g</code> (guess) flag.
Skylighting will try its best but may fall back to plain
text for ambiguous code.</p>

<h3 id="line-numbers">Line Numbers</h3>

<p>Sometimes you want line numbers in your code blocks,
especially for tutorials or when referencing specific lines.
The new <code class="language-plaintext highlighter-rouge">--code-line-numbers</code> flag has you covered:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--code-highlight</span> pygments <span class="nt">--code-line-numbers</span> input.md</code></pre></div></div>

<p>This passes the appropriate options to the highlighter
(Pygments gets <code class="language-plaintext highlighter-rouge">linenos=1</code>, Skylighting gets the <code class="language-plaintext highlighter-rouge">-n</code> flag).
The result is nicely numbered code that&rsquo;s easy to reference.</p>

<h3 id="automatic-styling-in-standalone-mode">Automatic Styling in Standalone Mode</h3>

<p>When you use <code class="language-plaintext highlighter-rouge">--code-highlight</code> with <code class="language-plaintext highlighter-rouge">--standalone</code> and
don&rsquo;t specify a <code class="language-plaintext highlighter-rouge">--css FILE</code> option, Apex automatically
embeds GitHub-style syntax highlighting CSS in the document
head. This covers all the common class names used by both
Pygments and Skylighting, so your highlighted code looks
great out of the box.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--standalone</span> <span class="nt">--code-highlight</span> pygments input.md <span class="o">&gt;</span> output.html</code></pre></div></div>

<p>The embedded CSS uses a clean, GitHub-inspired color scheme
that works well with light backgrounds. Keywords are red,
strings are blue, comments are gray, and so on. If you want
different colors, you can always override with your own
stylesheet using the <code class="language-plaintext highlighter-rouge">--css</code> flag.</p>

<h3 id="multiple-stylesheets">Multiple Stylesheets</h3>

<p>Speaking of stylesheets, another improvement today is that
<code class="language-plaintext highlighter-rouge">--css</code> now accepts multiple files. You can either use the
flag multiple times or pass a comma-separated list:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Multiple flags</span>
apex <span class="nt">--standalone</span> <span class="nt">--css</span> base.css <span class="nt">--css</span> syntax.css input.md

<span class="c"># Comma-separated</span>
apex <span class="nt">--standalone</span> <span class="nt">--css</span> base.css,syntax.css,custom.css input.md</code></pre></div></div>

<p>All specified stylesheets are linked in the document head in
the order provided. If you use <code class="language-plaintext highlighter-rouge">--embed-css</code>, all of them
get embedded as inline <code class="language-plaintext highlighter-rouge">&lt;style&gt;</code> blocks.</p>

<h3 id="requirements">Requirements</h3>

<p>You&rsquo;ll need one of the external tools installed and
available in your PATH:</p>

<ul>
  <li>
    <p><strong>Pygments</strong>: Install with <code class="language-plaintext highlighter-rouge">pip install Pygments</code>. The
  <code class="language-plaintext highlighter-rouge">pygmentize</code> binary needs to be accessible.</p>
  </li>
  <li>
    <p><strong>Skylighting</strong>: Install with <code class="language-plaintext highlighter-rouge">cabal install skylighting</code>
  or get it with Homebrew (<code class="language-plaintext highlighter-rouge">brew install skylighting</code>). The
  <code class="language-plaintext highlighter-rouge">skylighting</code> binary needs to be accessible.
If the specified tool isn&rsquo;t found, Apex will print a warning
and leave your code blocks unstyled (but otherwise
functional).</p>
  </li>
</ul>

<h3 id="other-updates">Other Updates</h3>

<p>A few other things landed alongside the syntax highlighting
feature:</p>

<ul>
  <li>
    <p><strong>ANSI art logo</strong> in <code class="language-plaintext highlighter-rouge">--version</code> output, because why not?</p>

    <p><img src="https://brettterpstra.com//uploads/2026/01/Apex-Version-400.jpg" alt="Apex Version" /></p>
  </li>
  <li>
    <p><strong>Test runner badge mode</strong> (<code class="language-plaintext highlighter-rouge">--badge</code>) that outputs just
  the pass/fail count for CI badge generation</p>
  </li>
  <li>
    <p><strong>Improved test output</strong> in errors-only mode now only
  shows suite titles for suites with failures</p>
  </li>
  <li>
    <p><strong>Test infrastructure refactoring</strong> with new suite
  tracking helpers for cleaner, more maintainable tests</p>
  </li>
</ul>

<p>I think the syntax highlighting will be a very useful
feature, at least for people publishing code. Integrating
with external tools means Apex handles the Markdown parsing
and document structure, while battle-tested highlighters
handle the colorization. Best of both worlds.</p>


<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115888368589343553">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Built-in+Syntax+Highlighting+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fbuilt-in-syntax-highlighting-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fbuilt-in-syntax-highlighting-for-apex%2F&text=Built-in+Syntax+Highlighting+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fbuilt-in-syntax-highlighting-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17252486.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Markdown to Sendy update: button tags, template fixes]]></title>
    <link href="https://brett.trpstra.net/link/535/17252432/markdown-to-sendy-update-button-tags-template-fixes"/>
    <updated>2026-01-13T08:00:00-06:00</updated>
    <published>2026-01-13T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/13/markdown-to-sendy-update-button-tags-template-fixes</id>
    <content type="html"><![CDATA[
<p>I&rsquo;ve been working on a few updates to my <a href="https://github.com/ttscoff/mdtosendy">Markdown to Sendy script</a> that add some nice quality-of-life improvements for creating email campaigns. The main additions are support for greeting/salutation customization and a new button liquid tag that makes it easier to create styled call-to-action buttons.</p>

<h2 id="greetings-and-salutations">Greetings and Salutations</h2>

<p>The script now supports customizable greetings that can be set in your template configuration. You can define a default greeting in your <code class="language-plaintext highlighter-rouge">config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">template</span><span class="pi">:</span>
  <span class="na">greeting</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Hey</span><span class="nv"> </span><span class="s">[Name,fallback=there],'</span>
  <span class="c1"># or use the alias:</span>
  <span class="na">salutation</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Hey</span><span class="nv"> </span><span class="s">[Name,fallback=there],'</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">salutation</code> key takes precedence if both are defined, which is useful if you want to keep things semantic. The greeting will automatically appear after the header image and before the first paragraph in your email.</p>

<p>You can also override it in the YAML frontmatter of individual emails:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">title</span><span class="pi">:</span> <span class="s">My Email</span>
<span class="na">greeting</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Hello</span><span class="nv"> </span><span class="s">everyone,'</span></code></pre></div></div>

<p>Or use a liquid tag to place it anywhere in your markdown:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% greeting %}

This is the email content.</code></pre></div></div>

<p>You can even provide a custom greeting directly in the tag:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% greeting "Hi there" %}</code></pre></div></div>

<p>The greeting text can contain Markdown or HTML and will be processed accordingly. The <code class="language-plaintext highlighter-rouge">[Name,fallback=there]</code> syntax is a Sendy merge tag that gets replaced when the email is sent.</p>

<h2 id="button-liquid-tags">Button Liquid Tags</h2>

<p>One of the more tedious parts of creating email buttons has been remembering the exact syntax for applying classes. The new <code class="language-plaintext highlighter-rouge">{% button %}</code> liquid tag makes this much easier.</p>

<p>You can use it with named attributes:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% button class="primary" text="Click Here" url="https://example.com" %}
{% button text="Click Here" url="https://example.com" %}</code></pre></div></div>

<p>Or with positional arguments:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% button "Click Here" https://example.com %}
{% button alt "Click Here" https://example.com %}</code></pre></div></div>

<p>The second example uses the <code class="language-plaintext highlighter-rouge">alt</code> class, which maps to the secondary button style. You can also use <code class="language-plaintext highlighter-rouge">alt2</code> for tertiary buttons. These classes (<code class="language-plaintext highlighter-rouge">alt</code> and <code class="language-plaintext highlighter-rouge">alt2</code>) are aliases for <code class="language-plaintext highlighter-rouge">secondary</code> and <code class="language-plaintext highlighter-rouge">tertiary</code>, making it quick to type common variants.</p>

<h3 id="reference-style-links">Reference-Style Links</h3>

<p>Button tags also support markdown reference-style links, which is great for keeping your markdown clean:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">[</span><span class="ss">Forum</span><span class="p">]:</span> <span class="sx">https://forum.brettterpstra.com</span>
<span class="p">[</span><span class="ss">Markdown Web</span><span class="p">]:</span> <span class="sx">https://forum.brettterpstra.com/t/how-about-a-markdown-web/4412/12</span>

{% button "Join the Conversation!" [Forum] %}
{% button alt "How about a Markdown Web?" [Markdown Web] %}</code></pre></div></div>

<p>Reference matching is case-insensitive, so <code class="language-plaintext highlighter-rouge">[Forum]</code>, <code class="language-plaintext highlighter-rouge">[forum]</code>, and <code class="language-plaintext highlighter-rouge">[FORUM]</code> all work the same way.</p>

<blockquote class="warn">
  <p>Note that button creation and styling requires a Markdown processor that handles IALs, such as Kramdown or Apex. I recommend installing <a href="https://github.com/ApexMarkdown/apex">Apex</a> for this tool.</p>
</blockquote>

<p>I know this is a project with a niche audience, but I&rsquo;m aware of a few people are already getting some use out of this. I know I personally love it. If you publish a newsletter or send marketing emails, let me know what you&rsquo;d need to see before it was useful in your own situation.</p>

<p>Check the script out <a href="https://github.com/ttscoff/mdtosendy">on GitHub</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115888185336445211">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Markdown+to+Sendy+update%3A+button+tags%2C+template+fixes%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fmarkdown-to-sendy-update-button-tags-template-fixes%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fmarkdown-to-sendy-update-button-tags-template-fixes%2F&text=Markdown+to+Sendy+update%3A+button+tags%2C+template+fixes&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fmarkdown-to-sendy-update-button-tags-template-fixes%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17252432.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for January 12nd, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17251867/web-excursions-for-january-12nd-2026"/>
    <updated>2026-01-12T12:00:00-06:00</updated>
    <published>2026-01-12T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/12/web-excursions-for-january-12nd-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with <a href="https://www.git-tower.com/?via=brett">Tower</a>, the absolute best Git GUI for macOS.</p>

<dl>
  <dt><a href="https://www.xmunch.com//?file=moss%3A%2F%2Fwww.xmunch.com%2Fcontent%2Fnodes%2Fmoss%2Findex.md">moss://xmunch.com</a></dt>
  <dd>Right in line with what I was thinking with my Markdown Web brainstorming. A protocol for content distribution that requires Markdown &mdash; portable, secure, and durable.</dd>
  <dt><a href="https://github.com/NSHipster/sosumi.ai">NSHipster/sosumi.ai: Making Apple docs AI-readable</a></dt>
  <dd>A web app and MCP server for converting Apple API docs into AI-compatible formats. Another way to save on tokens and get better results.</dd>
  <dt><a href="https://github.com/karol-broda/snitch">karol-broda/snitch: a prettier way to inspect network connections</a></dt>
  <dd>Great little CLI for inspecting network connections that&rsquo;s friendlier than <code class="language-plaintext highlighter-rouge">ss</code>/<code class="language-plaintext highlighter-rouge">netstat</code>.</dd>
  <dt><a href="https://justcontent.app/">JustContent</a></dt>
  <dd>Another (better?) take on markdownification. Very similar to <a href="https://markdownrules.om">Marky</a> but I think it might get even better results. Works well as a <code class="language-plaintext highlighter-rouge">curl</code> command, and while it can output markdown the same way Gather or Marky does, it&rsquo;s ostensibly designed as a better <code class="language-plaintext highlighter-rouge">curl</code> for your LLMs, reducing tokens and context.</dd>
  <dt><a href="https://www.anildash.com/2026/01/09/how-markdown-took-over-the-world/?__readwiseLocation=">How Markdown took over the world</a></dt>
  <dd>Excellent article from Anil Dash documenting the rise and rapid ubiquity of Markdown.</dd>
  <dd>
    <p>(The banner image on this post is pretty amazing as well.)</p>
  </dd>
</dl>

<p>If you&rsquo;re using Git, you need Tower. <a href="https://www.git-tower.com/?via=brett">Check it out today</a></p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115883477651073263">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+January+12nd%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fweb-excursions-for-january-12nd-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fweb-excursions-for-january-12nd-2026%2F&text=Web+Excursions+for+January+12nd%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fweb-excursions-for-january-12nd-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17251867.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7530.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Apex gets tables right]]></title>
    <link href="https://brett.trpstra.net/link/535/17251719/apex-gets-tables-right"/>
    <updated>2026-01-12T08:00:00-06:00</updated>
    <published>2026-01-12T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/12/apex-gets-tables-right</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7530.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Tables in Markdown have always been a bit of a mess. Every processor handles them slightly differently, and when you start wanting advanced features like column spans or captions, you&rsquo;re usually out of luck. I&rsquo;ve been working on <a href="https://github.com/ApexMarkdown/apex">Apex</a>, my unified Markdown processor, and I&rsquo;m happy to say that tables are now pretty solid.</p>

<h3 id="the-problem-with-empty-cells">The Problem With Empty Cells</h3>

<p>One of the trickier issues I ran into was how to handle empty cells versus colspan syntax. In many table implementations, consecutive pipes (<code class="language-plaintext highlighter-rouge">||</code>) create a column span. But what about cells that are just&hellip; empty? You know, with some whitespace between the pipes for alignment?</p>

<p>Previously, Apex was a little too eager to merge cells. If you had <code class="language-plaintext highlighter-rouge">|    |</code> (an empty cell with spaces), it might get treated as part of a colspan. That&rsquo;s not what anyone wants.</p>

<p>Now the rule is simple and predictable:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">||</code> or <code class="language-plaintext highlighter-rouge">|||</code> (consecutive pipes with nothing between them) = colspan</li>
  <li><code class="language-plaintext highlighter-rouge">|    |</code> (pipes with whitespace) = empty cell</li>
</ul>

<p>This means your carefully aligned Markdown source stays readable without accidentally merging cells.</p>

<h3 id="the-marker-for-explicit-colspans">The &laquo;&nbsp;Marker for Explicit Colspans</h3>

<p>Sometimes you want a colspan but you also want your source Markdown to look nice and aligned. That&rsquo;s where the new <code class="language-plaintext highlighter-rouge">&lt;&lt;</code> marker comes in:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| A   | B   | C   |
| --- | --- | --- |
| D   | &lt;&lt;  | &lt;&lt;  |</code></pre></div></div>

<p>Each <code class="language-plaintext highlighter-rouge">&lt;&lt;</code> indicates &ldquo;this cell is part of a colspan from the left.&rdquo; In this example, <code class="language-plaintext highlighter-rouge">D</code> spans all three columns. Your source stays perfectly aligned, and you get the colspan you wanted.</p>

<h3 id="row-spans-with-">Row Spans with ^^</h3>

<p>Column spans get all the attention, but row spans are just as useful. Use <code class="language-plaintext highlighter-rouge">^^</code> in a cell to merge it with the cell above:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| Department  | Employee |
| ----------- | -------- |
| Engineering | Alice    |
| ^^          | Bob      |
| ^^          | Charlie  |</code></pre></div></div>

<p>Here, &ldquo;Engineering&rdquo; spans three rows. Stack multiple <code class="language-plaintext highlighter-rouge">^^</code> markers and the rowspan grows accordingly.</p>

<h3 id="relaxed-tables">Relaxed Tables</h3>

<p>In unified and kramdown modes, you don&rsquo;t even need a separator row:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>one | two | three
1 | 2 | 3
4 | 5 | 6</code></pre></div></div>

<p>Apex figures out it&rsquo;s a table and renders it. No <code class="language-plaintext highlighter-rouge">|---|---|---|</code> required.</p>

<h3 id="headerless-tables">Headerless Tables</h3>

<p>Sometimes you just want data without a header row. Start with an alignment row:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| --- | :---: | ---: |
| left | center | right |
| a | b | c |</code></pre></div></div>

<p>The alignment row sets the column alignment, and the rest is body data. No phantom header row cluttering up your HTML.</p>

<h3 id="row-header-columns">Row Header Columns</h3>

<p>Need semantic row headers? Leave the first header cell empty:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>|       | Q1  | Q2  |
| ----- | --- | --- |
| Sales | 100 | 120 |
| Costs | 80  | 90  |</code></pre></div></div>

<p>Apex renders the first column of each body row as <code class="language-plaintext highlighter-rouge">&lt;th scope="row"&gt;</code>, which is exactly what screen readers expect for accessible data tables.</p>

<h3 id="csv-and-tsv-tables">CSV and TSV Tables</h3>

<p>Don&rsquo;t feel like typing all those pipes? Use a fenced code block with the <code class="language-plaintext highlighter-rouge">table</code> info string:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">```</span><span class="nl">table
</span><span class="sb">header 1,header 2,header 3
data 1,data 2,data 3</span>
<span class="p">```</span></code></pre></div></div>

<p>Apex auto-detects whether it&rsquo;s CSV or TSV and converts it to a proper table. You can even include an alignment row using keywords like <code class="language-plaintext highlighter-rouge">left</code>, <code class="language-plaintext highlighter-rouge">center</code>, <code class="language-plaintext highlighter-rouge">right</code>.</p>

<h3 id="multiple-caption-formats">Multiple Caption Formats</h3>

<p>Captions can go before or after tables, and Apex supports several syntaxes:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>[My Table Caption]

| A | B |
|---|---|
| 1 | 2 |</code></pre></div></div>

<p>Or Pandoc-style:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| A | B |
|---|---|
| 1 | 2 |

Table: My Table Caption</code></pre></div></div>

<p>Or the Pandoc table attributes extension:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| A | B |
|---|---|
| 1 | 2 |

: My Caption {#table-id .my-class}</code></pre></div></div>

<p>That last one even lets you attach IAL attributes directly to the table through the caption. Use <code class="language-plaintext highlighter-rouge">--captions above</code> or <code class="language-plaintext highlighter-rouge">--captions below</code> to control placement.</p>

<h3 id="table-footers">Table Footers</h3>

<p>Need a footer section? Use a row of <code class="language-plaintext highlighter-rouge">===</code>:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| Item   | Price |
| ------ | ----: |
| Widget | 10.00 |
| Gadget | 25.00 |
| ====== | ===== |
| Total  | 35.00 |</code></pre></div></div>

<p>Everything after the <code class="language-plaintext highlighter-rouge">===</code> row goes into <code class="language-plaintext highlighter-rouge">&lt;tfoot&gt;</code>.</p>

<h3 id="per-cell-alignment">Per-Cell Alignment</h3>

<p>Override column alignment on individual cells using colons:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| A      | B       | C        |
| ------ | ------- | -------- |
| :Left  | Right:  | :Center: |</code></pre></div></div>

<p>The colons are stripped from the output and replaced with inline styles.</p>

<h3 id="wrapping-up">Wrapping Up</h3>

<p>Tables were one of the areas where I really wanted Apex to shine. Between relaxed parsing, multiple colspan/rowspan syntaxes, CSV support, captions, footers, and per-cell alignment, I think it covers pretty much every table scenario I&rsquo;ve ever needed.</p>

<p>Check out the <a href="https://github.com/ApexMarkdown/apex">Apex repository</a> and the <a href="https://github.com/ApexMarkdown/apex/wiki/Tables">Tables documentation</a> for more details. And if you find edge cases I haven&rsquo;t covered, <a href="https://github.com/ApexMarkdown/apex/issues">open an issue on GitHub</a>. I&rsquo;m always looking to make tables even better.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115882519497199452">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Apex+gets+tables+right%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fapex-gets-tables-right%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fapex-gets-tables-right%2F&text=Apex+gets+tables+right&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fapex-gets-tables-right%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17251719.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7530.jpg"/>
  </entry>
</feed>
