<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Get Info: #workflow</title>
    <description>Posts tagged “workflow” — Blog of independent game and app developer Matt Sephton. Featuring vintage Macintosh, game development, digital artwork, Japanese esoterica, video game reviews, hacks and tips, and much more.</description>
    <link>https://blog.gingerbeardman.com/tag/workflow/</link>
    <atom:link href="https://blog.gingerbeardman.com/tag/workflow/index.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Sun, 12 Jul 2026 20:54:08 +0000</pubDate>
    <lastBuildDate>Sun, 12 Jul 2026 20:54:08 +0000</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>

    
      
        <item>
          <title>Transparent compression with Folder Actions in macOS</title>
          <description>&lt;p&gt;I just wired this up for my own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt; folder—where my current project is quietly piling up data—and figured it was worth writing down. The same trick works on any big codebase, a pile of assets, or a crowded &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/Applications&lt;/code&gt; folder. I generally use it on folders that don’t have frequent churn, but do have large or compressible things added to them often enough.&lt;/p&gt;

&lt;p&gt;It relies on &lt;em&gt;transparent file system compression&lt;/em&gt; (sometimes called AFSC), which has been built into macOS for years. It’s nothing like a zip archive: files aren’t bundled up or locked away—they keep their names, stay right where they are, and every app opens them as normal. macOS simply stores them compressed on disk and unpacks them on the fly as they’re read. The only thing that changes is the &lt;em&gt;size on disk&lt;/em&gt;—often dramatically. Apps can even launch more quickly, which is why all Mac App Store downloads already have this sort of filesystem compression applied.&lt;/p&gt;

&lt;p&gt;The classic tool for this is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;afsctool&lt;/code&gt;, but these days I reach for &lt;a href=&quot;https://github.com/Dr-Emann/applesauce&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applesauce&lt;/code&gt;&lt;/a&gt;, a modern Rust rewrite. It compresses much faster by working on blocks in parallel, uses less memory, and writes safely via atomic temp-file renames so you can’t corrupt a file.&lt;/p&gt;

&lt;p&gt;Rather than run it by hand in the Terminal or from a &lt;a href=&quot;/2024/07/30/taking-command-of-the-context-menu-in-macos/&quot;&gt;context menu&lt;/a&gt; every time, we can wire it into a &lt;em&gt;Folder Action&lt;/em&gt; so files get compressed the moment they land in a folder.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;install-applesauce&quot;&gt;Install applesauce&lt;/h2&gt;

&lt;p&gt;Two easy options:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Homebrew (lives in a custom tap, not the core repo)&lt;/span&gt;
brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;Dr-Emann/homebrew-tap/applesauce

&lt;span class=&quot;c&quot;&gt;# or the standalone installer, if you&apos;d rather skip brew&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;--proto&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;=https&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--tlsv1&lt;/span&gt;.2 &lt;span class=&quot;nt&quot;&gt;-LsSf&lt;/span&gt; https://github.com/Dr-Emann/applesauce/releases/latest/download/applesauce-cli-installer.sh | sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then find where it landed:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;which applesauce
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On Apple silicon &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brew&lt;/code&gt; installs to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/opt/homebrew/bin/&lt;/code&gt;; older Intel Macs use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/bin/&lt;/code&gt;. We’ll need that path in a moment.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;create-the-folder-action&quot;&gt;Create the Folder Action&lt;/h2&gt;

&lt;p&gt;Folder Actions is a built-in macOS directory watcher. We’ll build one with Automator:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Open &lt;em&gt;Automator&lt;/em&gt;, choose &lt;em&gt;New Document&lt;/em&gt;, pick &lt;em&gt;Folder Action&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;At the top, set &lt;em&gt;receives files and folders added to:&lt;/em&gt; to your target folder.&lt;/li&gt;
  &lt;li&gt;If Automator drops in a &lt;em&gt;Get Selected Finder Items&lt;/em&gt; action, delete it—Folder Actions already pass incoming files along, so keeping it breaks the pipeline.&lt;/li&gt;
  &lt;li&gt;Search the sidebar for &lt;em&gt;Run AppleScript&lt;/em&gt; and drag it into the workflow.&lt;/li&gt;
  &lt;li&gt;Paste this, swapping the path if yours differs:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-applescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;theItem&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
		&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
			&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;do shell script&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/opt/homebrew/bin/applesauce compress &quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;quoted form&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;POSIX&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;theItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
		&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Save (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cmd + S&lt;/code&gt;) as something descriptive like “Compress”, drop a few files into the folder, and macOS shrinks them behind the scenes. Pretty easy!&lt;/p&gt;

&lt;p&gt;Just for you, here’s one I made earlier: &lt;a href=&quot;https://cdn.gingerbeardman.com/files/Compress.workflow.zip&quot;&gt;Compress.workflow.zip&lt;/a&gt; (93 KB)&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;no-sudo-required&quot;&gt;No sudo required&lt;/h2&gt;

&lt;p&gt;Changing a file’s compression &lt;em&gt;sounds&lt;/em&gt; like root territory, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applesauce&lt;/code&gt; only writes to your own user-owned folders, so it isn’t. That means no Touch ID or password prompts breaking your flow, and nothing running with privileges it doesn’t need.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;what-triggers-it&quot;&gt;What triggers it&lt;/h2&gt;

&lt;p&gt;Worth knowing before you lean on this: a Folder Action only fires when items are &lt;em&gt;added&lt;/em&gt; to the folder (or removed from it), never when a file already sitting inside it changes. It isn’t recursive either—only the top level of the folder is watched, not the contents of any subfolders. That’s &lt;a href=&quot;https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/WatchFolders.html&quot;&gt;straight from Apple’s docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/Applications&lt;/code&gt; that comes with a catch. When an app updates itself it usually swaps in a whole new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.app&lt;/code&gt; bundle, which is technically a new item—but Folder Actions have always been flaky at noticing changes made by anything other than Finder, and updaters move files into place programmatically. So don’t count on self-updating apps getting re-compressed for you. When you want to be sure, drag the app out and back in, or just re-run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applesauce compress&lt;/code&gt; on it by hand.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;choosing-the-algorithm&quot;&gt;Choosing the algorithm&lt;/h2&gt;

&lt;p&gt;By default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applesauce compress&lt;/code&gt; uses &lt;em&gt;LZFSE&lt;/em&gt;, Apple’s fast and well-balanced algorithm—a good pick for almost everything. Two others are available:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;LZVN&lt;/em&gt; — tuned for 64-bit, very fast to decompress.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;ZLIB&lt;/em&gt; — slower to compress, but often the best ratio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Force one with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-c&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-applescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;do shell script&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/opt/homebrew/bin/applesauce compress -c LZVN &quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;quoted form&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;POSIX&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;theItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applesauce info &amp;lt;path&amp;gt;&lt;/code&gt; will report the ratio and algorithm any file ended up with. You beauty!&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;managing-setup&quot;&gt;Managing setup&lt;/h2&gt;

&lt;p&gt;Saving from Automator attaches the action to the folder you chose, but you can wire the same “Compress” action to more folders—or check it’s switched on if nothing’s happening—from the &lt;em&gt;Folder Actions Setup&lt;/em&gt; dialog. Right-click any folder in Finder and choose &lt;em&gt;Services → Folder Actions Setup…&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Tick &lt;em&gt;Enable Folder Actions&lt;/em&gt; (bottom-left).&lt;/li&gt;
  &lt;li&gt;Add the folder you want watched to the left column.&lt;/li&gt;
  &lt;li&gt;Pick “Compress” from the actions on the right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now anything you drop into that folder gets squeezed automatically, no matter how it got there.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/folder-actions.png&quot; alt=&quot;IMG&quot; title=&quot;Compress workflow applied to build folder&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;

&lt;p&gt;We can confirm the savings with Finder’s Get Info (wouldn’t that be a great name for a blog?). The &lt;em&gt;Size&lt;/em&gt; line still reports the full, uncompressed size—that’s what apps see—while the smaller figure in brackets is what the folder now actually takes up &lt;em&gt;on disk&lt;/em&gt;. The gap between the two is the saving.&lt;/p&gt;

&lt;p&gt;For finer detail, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applesauce info &amp;lt;path&amp;gt;&lt;/code&gt; prints the exact ratio and algorithm file by file.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/folder-actions-compression-results.png&quot; alt=&quot;IMG&quot; title=&quot;Check size on disk&quot; /&gt;&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Wed, 01 Jul 2026 15:44:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2026/07/01/transparent-compression-with-folder-actions-in-macos/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2026/07/01/transparent-compression-with-folder-actions-in-macos/</guid>
        </item>
      
    
      
        <item>
          <title>Fresh Files Extension for Nova editor</title>
          <description>&lt;p&gt;I spotted &lt;a href=&quot;https://github.com/FreHu/vscode-fresh-file-explorer&quot;&gt;Fresh File Explorer&lt;/a&gt;, a VS Code extension, &lt;a href=&quot;https://github.com/FreHu/vscode-fresh-file-explorer&quot;&gt;on Hacker News&lt;/a&gt; and loved the idea—a file sidebar that only shows recently modified files.&lt;/p&gt;

&lt;p&gt;The default file sidebar in an editor shows &lt;em&gt;everything&lt;/em&gt;, which in a large project is mostly noise. I thought it would be fun to have something like this for &lt;a href=&quot;https://nova.app&quot;&gt;Nova&lt;/a&gt;, so I reimplemented the concept from scratch using only the OG repo readme as a reference. No code is shared, it’s a completely new extension built against Nova’s API. That means I can’t do as much as the VS Code extension, such as colour coding the files like a heatmap, but it’s still quite useful as it is.&lt;/p&gt;

&lt;p&gt;The result is a sort of hybrid filesystem/git/bookmarks sidebar which I’m finding quite pleasant to use.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/fresh-files-extension-for-nova.png&quot; alt=&quot;PNG&quot; title=&quot;Fresh Files showing pinned files and recent changes&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;

&lt;p&gt;In Git repositories, Fresh Files uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git log&lt;/code&gt; to figure out what’s changed. In non-Git workspaces it falls back to filesystem modification times, so it works in any folder. It shares some functionality with the built-in Git Sidebar but I’ve been careful to not duplicate too much.&lt;/p&gt;

&lt;p&gt;There are two modes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pending Changes&lt;/strong&gt; — shows uncommitted files (or files modified in the last day if there’s no Git)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Historical&lt;/strong&gt; — shows files modified within a configurable time window, from 1 hour to 360 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can toggle the time window from the sidebar header, the command palette, or project settings.&lt;/p&gt;

&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;

&lt;p&gt;The sidebar supports both a flat file list and a directory tree view, with sorting by recency or name. Files show relative timestamps like “2h ago” or “3d ago” so you can see at a glance what’s freshest.&lt;/p&gt;

&lt;p&gt;Other things worth mentioning:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pinned files&lt;/strong&gt; — pin files to keep them visible regardless of time window&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;File history&lt;/strong&gt; — right-click any file to see its commit history with diffs&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Diff Search (Pickaxe)&lt;/strong&gt; — find commits where a string was added or removed, file-scoped from the sidebar or repo-wide from the command palette&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Line History&lt;/strong&gt; — view git history for the current line or selection&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Exhume&lt;/strong&gt; — view deleted file contents with syntax highlighting&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Resurrect&lt;/strong&gt; — restore deleted files to their original location&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Show All Files&lt;/strong&gt; — temporarily show all tracked files, overriding the time window&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Search Fresh Files&lt;/strong&gt; — full-text search across fresh files from the command palette&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;New File&lt;/strong&gt; — create a new file from the sidebar context menu&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Move to Trash&lt;/strong&gt; — delete files from the sidebar context menu&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Quick Open&lt;/strong&gt; — fuzzy-open from just the fresh files via the command palette&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Deleted file indicators&lt;/strong&gt; — deleted files show up with a distinct icon&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Auto-refresh&lt;/strong&gt; — the sidebar updates after saves, git commits, checkouts, and merges&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;context-menu&quot;&gt;Context menu&lt;/h2&gt;

&lt;p&gt;Right-click gives you New File, Show in Finder, Copy Path, Copy Relative Path, Move to Trash, Pin/Unpin, Show File History, Diff Search, and Exhume/Resurrect for deleted files.&lt;/p&gt;

&lt;h2 id=&quot;get-it-now&quot;&gt;Get it now&lt;/h2&gt;

&lt;p&gt;You can install it at: &lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.FreshFiles/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.FreshFiles/&lt;/a&gt;&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Tue, 24 Feb 2026 13:00:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2026/02/24/fresh-files-extension-for-nova-editor/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2026/02/24/fresh-files-extension-for-nova-editor/</guid>
        </item>
      
    
      
        <item>
          <title>Remove Comments Extension for Nova editor</title>
          <description>&lt;p&gt;I’ve released a new extension for the Nova editor.&lt;/p&gt;

&lt;p&gt;It’s called &lt;em&gt;Remove Comments&lt;/em&gt; and it …removes comments from the current line, or selected lines, in your code. That’s it!&lt;/p&gt;

&lt;p&gt;Oh, I’ve tried to support as many comment/syntax formats as I can think of. Sadly it’s not possible to get the current comment formatting from the Nova API, so I had to roll my own logic and heuristics.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.remove-comments/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.remove-comments/&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;examples&quot;&gt;Examples&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JavaScript (C-style comments):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// This comment will be removed&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// This trailing comment will be removed&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;/* This block comment will be removed */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After running Remove Comments:&lt;/p&gt;
&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Python (hash-style comments):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# This comment will be removed
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;42&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# This trailing comment will be removed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After running Remove Comments:&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;HTML:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- This comment will be removed --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Content&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- This trailing comment will be removed --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After running Remove Comments:&lt;/p&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Content&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Mon, 08 Dec 2025 15:00:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2025/12/08/remove-comments-extension-for-nova-editor/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2025/12/08/remove-comments-extension-for-nova-editor/</guid>
        </item>
      
    
      
        <item>
          <title>How to tame a user interface using a spreadsheet</title>
          <description>&lt;p&gt;Many years ago, while working at Apple and running a lab at WWDC, I met a guy who was using a piece of &lt;a href=&quot;https://en.wikipedia.org/wiki/IAd_Producer&quot;&gt;Apple software designed for creating interactive ads&lt;/a&gt; to design a Car Play user interface for a popular US car manufacturer. I was impressed by his ability to think outside the box and told him so. I mentioned to him how human interface designers at Apple &lt;a href=&quot;https://www.youtube.com/watch?v=DGn7BcFGigc&amp;amp;t=474s&quot;&gt;were using Keynote to rapidly prototype user interfaces and animations&lt;/a&gt;. The discussion then took a strange turn onto spreadsheets.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;spreadsheets&quot;&gt;Spreadsheets?&lt;/h2&gt;

&lt;p&gt;In my opinion, spreadsheets are one of the greatest user interface design tools ever created. I believe that the most effective tool for a job is one that gets out of the way as much as possible. Even better if it’s a tool that you already have access to, or at the very least, can use at no cost.&lt;/p&gt;

&lt;p&gt;Before you have time to load up Figma, Sketch, Photoshop, or any other design software, I’ll have already typed in my content placeholders in a spreadsheet and be most of my way towards a solution. I’ll be merging and moving cells at lightning speed—rationalising layouts, determining the optimal use of space, and creating order out of chaos.&lt;/p&gt;

&lt;p&gt;The purpose of using a tool that may seem ill-suited to the task at hand—designing user interfaces—is to liberate yourself from the clutter or baggage of content and style. This allows you to focus solely on layout, positioning, and hierarchy. You don’t have to worry about type sizes, or get bogged down with software choices. You can quickly adjust the size or structure of the screen by adding/removing rows/columns. With a spreadsheet, it’s just the keyboard between you and the perfect layout.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;lets-take-your-brain-to-another-dimension&quot;&gt;Let’s take your brain to another dimension&lt;/h2&gt;

&lt;p&gt;One frequent criticism of spreadsheets is that they are “only 2D” or that a grid isn’t versatile enough to solve your problem. But if you pay close attention, you’ll realise that spreadsheets can occupy as many dimensions as you require by using multiple sheets. Like the stack of ledger paper that was used before spreadsheets were invented. An episode of The Computer Chronicles called “The Spreadsheet Wars” aired in 1988 and has a great &lt;a href=&quot;https://youtu.be/eAiZBUYNUBA?si=TMiXXLj1gtFwZQkp&amp;amp;t=1154&quot;&gt;demo of spreadsheet software Lotus 1-2-3 doing 3D spreadsheet manipulation&lt;/a&gt; in ways that are still impressive several decades later.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://archive.org/details/GridSystemsInGraphicDesignJosefMullerBrockmann&quot;&gt;Grid systems are well known across many fields of design&lt;/a&gt; for being a huge assist when solving layouts and rationalising problems. The grid enforces structure invisibly during the design process. A spreadsheet is a highly configurable grid system that is ready to be put to work on your problem!&lt;/p&gt;

&lt;p&gt;You might also consider the use of plain paper, graph paper, and a bitmap editor. Which might be best suited for sketching out a quick icon idea? It’s easy to understand that graph paper is more suitable than plain paper for drawing images made up of pixels, and in many situations it may be quicker and more suitable than a bitmap editor. &lt;a href=&quot;https://www.moma.org/collection/works/188382?artist_id=38483&amp;amp;page=1&amp;amp;sov_referrer=artist&quot;&gt;Susan Kare famously used a book of grid/graph paper to draw many early Macintosh icons&lt;/a&gt; because icon creation software had yet to be invented.&lt;/p&gt;

&lt;h2 id=&quot;theres-an-app-for-that&quot;&gt;There’s an app for that&lt;/h2&gt;

&lt;p&gt;I use Apple Numbers to do most of my spreadsheet work, because it’s the one that I have easiest access to most of the time—it’s installed on both my Mac and iPhone. You might want to use Google Sheets, Microsoft Excel, or even a simpler table tool like &lt;a href=&quot;https://smoothcsv.com&quot;&gt;SmoothCSV&lt;/a&gt;. There is no wrong choice.&lt;/p&gt;

&lt;p&gt;Here are some examples of layouts I’ve solved using a spreadsheet.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;yoyozo&quot;&gt;YOYOZO&lt;/h2&gt;

&lt;p&gt;Perhaps my most famous example is the stats screen of &lt;a href=&quot;/2023/11/21/yoyozo-how-i-made-a-playdate-game-in-39kb/&quot;&gt;my hit game &lt;em&gt;YOYOZO&lt;/em&gt;&lt;/a&gt;. With this one I already had a TTF font file that I’d created for the manual, so I went the extra mile and planned a pixel perfect layout in the right colours and everything. A fun fact you might not know: &lt;em&gt;YOYOZO&lt;/em&gt; was featured in Ars Technica’s “Best Games of 2023” list, alongside Mario and Zelda. This spreadsheet thing is great!&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/spreadsheet-ui-yoyozo.png&quot; alt=&quot;IMG&quot; title=&quot;Pixel perfect precision using the exact font&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;enhanced-sfxr&quot;&gt;Enhanced sfxr&lt;/h2&gt;

&lt;p&gt;This year I’ve made two enhanced versions of sfxr—&lt;a href=&quot;https://gingerbeardman.itch.io/enhanced-sfxr-for-love2d&quot;&gt;one for Love2D&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/search?q=from%3Agingerbeardman%20sfxr&amp;amp;src=typed_query&amp;amp;f=live&quot;&gt;one for macOS&lt;/a&gt;—and I can’t remember which one this was done for. I usually get rid of the header rows and columns but in this instance they acted as sidebar and title bar. Regardless, whilst it might not look like much this sort of pre-planning and organisation saved a ton of time when I was working on the app. It involved no code and was done in a minute or two!&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/spreadsheet-ui-sfxr.png&quot; alt=&quot;IMG&quot; title=&quot;Organising the large components that make up the window&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;spindle&quot;&gt;Spindle&lt;/h2&gt;

&lt;p&gt;Another of my “coming soon” apps is &lt;a href=&quot;https://twitter.com/search?q=from:gingerbeardman%20spindle&amp;amp;src=typed_query&amp;amp;f=live&quot;&gt;an optical media dumper whose USP is its ability to dump from multiple drives at the same time&lt;/a&gt;. I wanted a UI that would stay mostly the same across both the setup and dumping states, and also that was easily repeatable when additional drives duplicate the app UI. The benefit of working with a grid here is that it was 1:1 transferrable to SwiftUI grid system!&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/spreadsheet-ui-spindle.png&quot; alt=&quot;IMG&quot; title=&quot;Two states of the same interface showing what changes and what remains the same&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;driving-game&quot;&gt;Driving Game&lt;/h2&gt;

&lt;p&gt;This final one is an example of a racing game. Something that you might already be more familiar with and so be able to figure out how you can apply the technique to your own work. Something like Mario Kart, F-Zero, Colin McRae Rally, or WipEout. Let’s go!&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/spreadsheet-ui-racer.png&quot; alt=&quot;IMG&quot; title=&quot;A fairly standard driving/racing game head up display&quot; /&gt;&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Sat, 11 Oct 2025 00:03:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2025/10/11/how-to-tame-a-user-interface-using-a-spreadsheet/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2025/10/11/how-to-tame-a-user-interface-using-a-spreadsheet/</guid>
        </item>
      
    
      
        <item>
          <title>Three.js Completions Extension for Nova editor</title>
          <description>&lt;p&gt;I’ve released an extension to add &lt;a href=&quot;https://threejs.org&quot;&gt;Three.js&lt;/a&gt; completions support to the Nova editor.&lt;/p&gt;

&lt;p&gt;The coolest thing about this extension is that most of the code is automatically generated straight from the Three.js TypeScript files. This means there’s the potential for less errors in the data, and I can easily update the extension whenever a new release of Three.js comes out just by running a single command. Why work &lt;em&gt;harder&lt;/em&gt; when you can work &lt;em&gt;cleverer&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.ThreeJS/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.ThreeJS/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might be quick to deduce that—yes—I’m making a 3D game. 😘&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Sat, 04 Oct 2025 15:00:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2025/10/04/three-js-completions-extension-for-nova-editor/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2025/10/04/three-js-completions-extension-for-nova-editor/</guid>
        </item>
      
    
      
        <item>
          <title>Updates to Extensions for Nova editor</title>
          <description>&lt;p&gt;In late 2024, I spent some time improving my tools by &lt;a href=&quot;/2024/10/17/extensions-for-nova-editor/&quot;&gt;building a set of extensions for Nova editor&lt;/a&gt; to streamline some time-consuming tasks I encounter during blogging and game development.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/nova-extensions.png&quot; alt=&quot;IMG&quot; title=&quot;My current list of extensions for Nova editor&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;This week, I released major updates to many of them with enhancements and new features:&lt;/p&gt;

&lt;h2 id=&quot;bookmarks&quot;&gt;Bookmarks&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.Bookmarks/&quot;&gt;Version 2.0.0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Adds sorting, the ability to bookmark folders, marking of missing items, and other improvements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;markdown-file-linker&quot;&gt;Markdown File Linker&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.MarkdownFileLinker/&quot;&gt;Version 2.0.0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Results in the choice palette are now sorted by default using dates found in filenames.&lt;/li&gt;
  &lt;li&gt;Along with other filtering improvements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;unwrap-paragraph&quot;&gt;Unwrap Paragraph&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.unwraptext/&quot;&gt;Version 2.0.0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;You can now unwrap the text surrounding cursor, which removes some friction.&lt;/li&gt;
  &lt;li&gt;In selected text multiple paragraphs will be unwrapped individually.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;word-counter&quot;&gt;Word Counter&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.wordcounter/&quot;&gt;Version 2.0.0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Watched words can be managed through the sidebar, added from selected text, or via typing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;yaml-tag-picker&quot;&gt;YAML Tag Picker&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.YAMLTagPicker/&quot;&gt;Version 2.0.0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;You can quickly insert or update the creation/modified ISO date in your front matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;Plus!&lt;/em&gt; I also released a brand new extension:&lt;/p&gt;

&lt;h2 id=&quot;csv-to-md-table&quot;&gt;CSV to MD table&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.csv2md/&quot;&gt;Version 1.0.0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Convert tabular data between CSV/TSV and Markdown formats.&lt;/li&gt;
&lt;/ul&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Sat, 30 Aug 2025 15:03:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2025/08/30/updates-to-my-extensions-for-nova-editor/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2025/08/30/updates-to-my-extensions-for-nova-editor/</guid>
        </item>
      
    
      
        <item>
          <title>Word Counter extension for Nova editor</title>
          <description>&lt;p&gt;So the &lt;a href=&quot;/2024/10/24/macro-extension-for-nova-editor/&quot;&gt;Macro extension&lt;/a&gt; and &lt;a href=&quot;/2024/10/17/extensions-for-nova-editor/&quot;&gt;a bunch of smaller extensions&lt;/a&gt; were supposed to be it, but it’s so much fun to create these that whenever I get an idea for an extension I can’t help making it.&lt;/p&gt;

&lt;p&gt;This time I wanted to keep track of how many times I’m calling certain functions in my code. That’s quite a niche requirement so I made a more general purpose word counter with sidebar, thresholds and coloured blobs!&lt;/p&gt;

&lt;p&gt;You can configure a list of words to count, and the thesholds that will trigger the grey/green/yellow/red coloured blobs, on a per-project basis using the workspace &lt;em&gt;Project Settings…&lt;/em&gt; panel.&lt;/p&gt;

&lt;p&gt;And you can double-click on a word in the sidebar to search the current document for it. This feature uses AppleScript so you’ll need to give Nova permissions, though it should prompt you to do that if it doesn’t have them already.&lt;/p&gt;

&lt;p&gt;Note: the screenshot below is slightly old as I’ve since refined the images in the extension.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/nova-word-counter.png&quot; alt=&quot;PNG&quot; title=&quot;Counting some Playdate SDK specific function names&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;downloads&quot;&gt;Downloads&lt;/h2&gt;

&lt;p&gt;You can download it at: &lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.wordcounter/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.wordcounter/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the &lt;a href=&quot;https://github.com/gingerbeardman/Word-Counter&quot;&gt;source code is available on GitHub&lt;/a&gt; and PRs are more than welcome!&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Sun, 27 Oct 2024 17:12:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2024/10/27/word-counter-extension-for-nova-editor/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2024/10/27/word-counter-extension-for-nova-editor/</guid>
        </item>
      
    
      
        <item>
          <title>Macro extension for Nova editor</title>
          <description>&lt;p&gt;After creating &lt;a href=&quot;/2024/10/17/extensions-for-nova-editor/&quot;&gt;a bunch of smaller Nova Extensions quite quickly&lt;/a&gt; I wondered how far I could push things, just as a personal challenge. I had the idea of implementing a Macro text recording and playback system. There was something similar in TextMate editor, and whilst the Nova API doesn’t currently allow as sophisticated a system I thought it was something still worth exploring.&lt;/p&gt;

&lt;p&gt;Design of the system was incremental, with improvements and simplifications that came about through using it. It took a few aborted attempts to land on a good mechanism of recording and playing back “actions”, which currently encompass:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;insertions&lt;/li&gt;
  &lt;li&gt;deletions&lt;/li&gt;
  &lt;li&gt;cursor position changes&lt;/li&gt;
  &lt;li&gt;selection changes&lt;/li&gt;
  &lt;li&gt;selection replacements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I encode these changes with some simple JSON markup. Insertions are represented as text, and replacements as pairs of text, whilst deletions, cursor position changes, and selection changes are represented as numbers where positive means “to the right” and negative means “to the left”. It’s a simple system but should prove to be expandable if the Nova API is expanded to include extra capabilities, such as hooking into the &lt;em&gt;Find&lt;/em&gt;, &lt;em&gt;Files&lt;/em&gt; sidebar, or the &lt;em&gt;Command Palette&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Macro data can end up quite large, as we’re recording single character or position changes, so I added the ability to compress a macro by coalescing actions of the same type into one. A sort of naïve huffman encoding scheme. This makes a huge difference to both size and playback speed.&lt;/p&gt;

&lt;p&gt;You can download the extension at: &lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.Macro/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.Macro/&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;recording&quot;&gt;Recording&lt;/h2&gt;

&lt;p&gt;Recording is done on-demand, only when you want it. A notification will signal recording has begun. When you’ve finished you click the Stop button in the notification and the macro will be automatically saved to the sidebar with a sequential name. You can rename it afterwards if you’d like.&lt;/p&gt;

&lt;p&gt;You can also start the recording using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; sidebar icon, or using the command via the Editor menu or &lt;em&gt;Command Palette&lt;/em&gt;, but recording is always stopped through the notification. It took a while to arrive at this mechanism, and for a long time the extension was a lot more complicated and confusing to use.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/nova-macro-extension-record.png&quot; alt=&quot;IMG&quot; title=&quot;Macro recording notification&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;example-macro&quot;&gt;Example Macro&lt;/h2&gt;

&lt;p&gt;Let’s type:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Nova!&amp;lt;move left 1&amp;gt;&amp;lt;select -3&amp;gt;&amp;lt;replace &quot;ova&quot; → &quot;O&quot;&amp;gt;VA&amp;lt;move left 3&amp;gt;&amp;lt;delete 3&amp;gt;ova&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end result of which is:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Nova!&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we were to record this, the results would be…&lt;/p&gt;

&lt;p&gt;Raw macro (17 actions):&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Macro 1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;actions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;N&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;o&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;POS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;direction&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;←&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SEL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SEL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;-2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SEL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;-3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;REP&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;old&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ova&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;new&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;O&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;V&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;A&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;POS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;direction&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;←&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;POS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;direction&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;←&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;POS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;direction&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;←&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DEL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DEL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DEL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;o&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;isExpanded&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Compressed macro (6 actions):&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Nova!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;POS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;direction&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;←&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SEL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;-3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;REP&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;old&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ova&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;new&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;O&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;VA&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;POS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;direction&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;←&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DEL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ova&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Compression also helps with readability though I added a function to copy a human-readable version of the macro,which is how the &lt;a href=&quot;#example-macro&quot;&gt;text at the start of this example&lt;/a&gt; was generated.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/nova-macro-extension-anim.gif&quot; alt=&quot;IMG&quot; title=&quot;Playback of the above macro&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;sidebar&quot;&gt;Sidebar&lt;/h2&gt;

&lt;p&gt;Recorded macros live in a bespoke sidebar, and can be expanded or collapsed on demand. This allows us to see each action in the macro and exactly how the compressed version differs. The context menu provides functions to manipulate and manage the macros.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/nova-macro-extension-sidebar.png&quot; alt=&quot;IMG&quot; title=&quot;Macro sidebar with raw and compressed versions, expanded to show all actions&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;context-menu&quot;&gt;Context Menu&lt;/h3&gt;

&lt;p&gt;After a macro is recorded you can manipulate it using the context menu of items in the Macro sidebar:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Replay&lt;/li&gt;
  &lt;li&gt;Compress&lt;/li&gt;
  &lt;li&gt;Duplicate&lt;/li&gt;
  &lt;li&gt;Rename&lt;/li&gt;
  &lt;li&gt;Copy (Compressed)&lt;/li&gt;
  &lt;li&gt;Copy (Raw)&lt;/li&gt;
  &lt;li&gt;Copy (Readable)&lt;/li&gt;
  &lt;li&gt;Delete&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;configuration&quot;&gt;Configuration&lt;/h2&gt;

&lt;p&gt;Recording selections adds extra weight and slowness to a macro, so whilst the extension defaults to recording text selection changes I added the option to switch it off. If you’re only interested in the smallest size macros you can enable automatic compression of every macro which happens as the macro is saved.&lt;/p&gt;

&lt;p&gt;Playback is very quick but in some situations, such as playing back for a screen recording, you might want the text to appear more slowly, so I added a playback speed option.&lt;/p&gt;

&lt;p&gt;Note: if you want to see the smoothest character-by-character playback, you should make sure compression and record selection changes are both switched off.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;improvements&quot;&gt;Improvements&lt;/h2&gt;

&lt;p&gt;There are still some improvements I’d like to make in the future: to be able to more easily see the cursor during playback, and to highlight the current action in the sidebar so you can keep track of progress. We’ll see if/when I get around to those.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/gingerbeardman/Macro&quot;&gt;source code is available on GitHub&lt;/a&gt; and PRs are more than welcome!&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Thu, 24 Oct 2024 14:02:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2024/10/24/macro-extension-for-nova-editor/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2024/10/24/macro-extension-for-nova-editor/</guid>
        </item>
      
    
      
        <item>
          <title>Extensions for Nova editor</title>
          <description>&lt;p&gt;I’m a big believer in solving problems yourself if it’s possible rather than waiting for app updates that might never arrive. Making extensions for the &lt;a href=&quot;https://nova.app&quot;&gt;Nova editor&lt;/a&gt; that I do most of my programming and blogging in is so much fun! So, here are some of my own creation:&lt;/p&gt;

&lt;p&gt;View all: &lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;yaml-tag-picker&quot;&gt;YAML Tag Picker&lt;/h2&gt;

&lt;p&gt;Allows you to easily select tags for the front matter in your blog posts. It scans your existing posts for tags and presents them in a Choice Palette, making it easy to maintain consistent tagging across your blog. Nice!&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.YAMLTagPicker/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.YAMLTagPicker/&lt;/a&gt;&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/yaml-tag-picker.png&quot; alt=&quot;IMG&quot; title=&quot;Searching existing tags for the word “play”&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;markdown-file-linker&quot;&gt;Markdown File Linker&lt;/h2&gt;

&lt;p&gt;Allows you to insert links to local files as Markdown, perfect for linking between articles in your Jekyll blog! Also includes the ability to wrap the current text in a link, and if there’s a URL on the clipboard that’ll be used as the link destination.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.MarkdownFileLinker/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.MarkdownFileLinker/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You choose a local file using the file selector, such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/Users/matt/Projects/blog/_posts/2023/2023-11-21-yoyozo-how-i-made-a-playdate-game-in-39kb.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it will be inserted as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/2023/11/21/yoyozo-how-i-made-a-playdate-game-in-39kb/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’ve selected some text before invoking the extension, you’ll get:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[YOYOZO](/2023/11/21/yoyozo-how-i-made-a-playdate-game-in-39kb/)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an image you might end up with:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;![IMG](∕images/posts/yoyozo-teaser.gif)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;reindent-o-matic&quot;&gt;Reindent-o-matic&lt;/h2&gt;

&lt;p&gt;Allows you to apply &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.editorconfig&lt;/code&gt; indent rules to the current file, or all files matching specific extensions. Important: changes are applied but not saved, giving you the opportunity to review.&lt;/p&gt;

&lt;p&gt;Useful when reusing code from one project that used space indentation to a new one that uses tab indentation. Ideally Nova should do this automatically, and sometimes it does, but not every time for reasons I can’t figure out.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.Reindent-o-matic/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.Reindent-o-matic/&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;toggle-scroll-bars&quot;&gt;Toggle Scroll Bars&lt;/h2&gt;

&lt;p&gt;Allows you to see the scrollbar at all times. This is useful because the scrollbar also contains source control change markers. With scrollbars always visible you can more easily locate changes across the entire length of your document.&lt;/p&gt;

&lt;p&gt;This has already saved me so much time and effort and I’ve only been using the editor this way for a week. So I thought I’d create an extension to make it easier for other people to enjoy this usability improvement.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.scrollbars/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.scrollbars/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scroll Bars on/off&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/scrollbars-on-minimap-off.png&quot; alt=&quot;on-off&quot; /&gt; &lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/scrollbars-off-minimap-off.png&quot; alt=&quot;off-off&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;br clear=&quot;both&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;unwrap-paragraph&quot;&gt;Unwrap Paragraph&lt;/h2&gt;

&lt;p&gt;OK, I got carried away. This one adds the ability to merge multiple lines of selected text into one.&lt;/p&gt;

&lt;p&gt;This is useful for combining multiple lines of data into one, or reflowing a paragraph of text that has had manual line breaks applied such as something pasted from an old email. It also colalesces areas of white space into a single space. So, it’s not just “Join Lines” but something more specific.&lt;/p&gt;

&lt;p&gt;In the &lt;em&gt;TextMate&lt;/em&gt; editor this command is called Unwrap Paragraph, so I’ve kept the same name.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.unwraptext/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.unwraptext/&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;example&quot;&gt;Example&lt;/h2&gt;

&lt;p&gt;Text, before:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;	What 
exactly does  
this   extension	
do with the
text?  🤔
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Text, after:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;What exactly does this extension do with the text? 🤔
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Source code, before:&lt;/p&gt;
&lt;div class=&quot;language-lua highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x050&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x104&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x202&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x050&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x451&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x104&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x505&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x088&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x272&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x104&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;mh&quot;&gt;0x050&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Source code, after:&lt;/p&gt;
&lt;div class=&quot;language-lua highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x050&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x104&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x202&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x050&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x451&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x104&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x505&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x088&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x272&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x104&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x050&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;filter-through-command&quot;&gt;Filter Through Command&lt;/h2&gt;

&lt;p&gt;Run terminal commands on selected text. When you need to use a terminal command but you just want the results. The possibilities are endless as command line is your oyster! Comes with a library of useful commands including sort, base64 decode/encode, extract URLs, remove blank lines, count loc, remove HTML tags.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.FilterThroughCommand/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.FilterThroughCommand/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type your own command&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/nova-filter-through-custom-command.png&quot; alt=&quot;PNG&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;br clear=&quot;both&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick from a library of commands&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/nova-filter-through-command.png&quot; alt=&quot;PNG&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;br clear=&quot;both&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;bookmarks&quot;&gt;Bookmarks&lt;/h2&gt;

&lt;p&gt;Bookmark files in the Sidebar for easier management. Ideal if the Files Sidebar is too much, or you want a simpler view of the files you are working on.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.Bookmarks/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.Bookmarks/&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;tag-sync&quot;&gt;Tag Sync&lt;/h2&gt;

&lt;p&gt;Automatic synchronisation of closing tag when editing opening tag. Improve your coding efficiency by ensuring tag pairs stay synchronized while editing markup languages!&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.tagsync/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.tagsync/&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;openscad&quot;&gt;OpenSCAD&lt;/h2&gt;

&lt;p&gt;Adds language support for &lt;a href=&quot;https://openscad.org&quot;&gt;OpenSCAD&lt;/a&gt;: &lt;em&gt;The Programmers Solid 3D CAD Modeller&lt;/em&gt;, including both syntaxes (with highlighting) and completions (with alternate forms).&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.openscad/&quot;&gt;extensions.panic.com/extensions/com.gingerbeardman/com.gingerbeardman.openscad/&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;macro&quot;&gt;Macro&lt;/h2&gt;

&lt;p&gt;Text editing recording and playback system. This one &lt;a href=&quot;/2024/10/24/macro-extension-for-nova-editor/&quot;&gt;has its own blog post&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;word-counter&quot;&gt;Word Counter&lt;/h2&gt;

&lt;p&gt;Count multiple words and have their tally displayed in the sidebar. This one also &lt;a href=&quot;/2024/10/27/word-counter-extension-for-nova-editor/&quot;&gt;has its own blog post&lt;/a&gt;.&lt;/p&gt;

</description>
          <author>by Matt Sephton</author>
          <pubDate>Thu, 17 Oct 2024 20:47:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2024/10/17/extensions-for-nova-editor/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2024/10/17/extensions-for-nova-editor/</guid>
        </item>
      
    
      
        <item>
          <title>Stapler: I remade a 32 year old classic Macintosh app</title>
          <description>&lt;blockquote&gt;
  &lt;p&gt;The latest version of this app is now available as a free download on the Mac App Store.&lt;br /&gt;
More info on my apps website at: &lt;a href=&quot;https://www.gingerbeardman.com/apps/stapler&quot;&gt;gingerbeardman.com/apps/stapler/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A couple of days ago I was &lt;a href=&quot;https://news.ycombinator.com/item?id=41192547&quot;&gt;reading on Hacker News about a feature in some &lt;em&gt;Linux&lt;/em&gt; window managers where they allow collections of tabs from different apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This reminded me of &lt;em&gt;BeOS&lt;/em&gt;, but at the same time it reminded me of an app from 1992 for classic Macintosh called &lt;a href=&quot;https://macintoshgarden.org/apps/stapler-11&quot;&gt;&lt;em&gt;Stapler&lt;/em&gt;&lt;/a&gt;, and how &lt;a href=&quot;https://twitter.com/gingerbeardman/status/1590051288951443456&quot;&gt;I’d talked about that&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/gingerbeardman/status/1596573654674034691&quot;&gt;it’s one-time spiritual successor&lt;/a&gt;, &lt;a href=&quot;http://hasseg.org/launchList/&quot;&gt;&lt;em&gt;LaunchList&lt;/em&gt;&lt;/a&gt;, in the past. These were both similar apps that allowed you to collect and launch all the apps, files, folders, documents, related to a specific task, thus saving time. Or as Ali Rantakari so succinctly put it in 2009: “&lt;a href=&quot;http://hasseg.org/blog/post/249/launching-lots-of-stuff-at-once-on-your-mac/&quot;&gt;Opening a Bunch of Stuff at Once on Your Mac&lt;/a&gt;”.&lt;/p&gt;

&lt;p&gt;So over the past day or so I built my own take on this concept for modern macOS!&lt;/p&gt;

&lt;p&gt;Whilst my app is inspired by both Stapler (right down to the name) and LaunchList, my app adds improvements and features of its own and is more of a hybrid of both. For example, Stapler didn’t have drag and drop and LaunchList didn’t have zero-click auto-launch. My goal was to keep it as simple as possible, but no simpler. To that end, there are no preferences or settings in the app.&lt;/p&gt;

&lt;hr /&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/stapler.png&quot; alt=&quot;PNG&quot; title=&quot;Stapler, for modern macOS (12.0 or newer)&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;tell-me-more&quot;&gt;Tell me more&lt;/h2&gt;

&lt;p&gt;The idea is you set up a Stapler Document per project containing related apps, files, folders, etc. Then you can open them all at once by launching the single document. Each document contains a list of aliases that can be managed, inspected, launched using the app. The key time-saver is that if you launch a Stapler Document directly, all the items in its list will automatically be launched. Cool!&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Work: text editor, run current game, pixel art editor, bitmap font app, todo list&lt;/li&gt;
  &lt;li&gt;Play: Music app, Hacker News app, Twitter app, script to position windows&lt;/li&gt;
  &lt;li&gt;Movie: run Caffeine to keep your computer on, shortcut to put displays to sleep&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s an odd way of thinking about working on a computer—it’s task-based rather than app-based or document-based. Indeed, some might say it’s an outdated way of approaching things. But I’m always &lt;a href=&quot;https://www.youtube.com/playlist?list=PLfF-zlMNYMd_ZioGb0BKdqwWnx0wCDSgU&quot;&gt;banging the drum&lt;/a&gt; about there being so much good stuff that was prematurely left behind along with the memories of System 7 (&lt;a href=&quot;/2021/04/17/turning-an-ipad-pro-into-the-ultimate-classic-macintosh/&quot;&gt;though I still use that&lt;/a&gt;), BeOS, OS/2, Amiga Workbench, GEM and the many alternative desktops of Atari ST, etc. So I thought I’d see if I could walk the walk as well as talk the talk.&lt;/p&gt;

&lt;h2 id=&quot;details&quot;&gt;Details&lt;/h2&gt;

&lt;p&gt;It’s written in Swift and SwiftUI and weighs in at 640KB, about one third of which is a multitude of icon files at many different sizes and resolutions. By creating a Document-Based App you get a ton of functionality for free, such as document/tab/window management, undo/redo (though I still needed to watch for it and refresh the app window), and much more.&lt;/p&gt;

&lt;p&gt;The editor is a native macOS window that’s kind of like list view in a file manager, or a spreadsheet, or a little folder…depending on your point of view. Plus some menu commands and keyboard equivalents. You can have multiple windows open at once, for example one window each for all your common tasks, giving you the ability to launch full tasks or parts of them with minimal effort.&lt;/p&gt;

&lt;p&gt;The items in each list are macOS &lt;a href=&quot;https://eclecticlight.co/2020/05/21/bookmarks-a-type-of-alias-their-access-and-use/&quot;&gt;bookmarks&lt;/a&gt; (sometimes: security scoped bookmarks) which are a type of authorised/verified/secure alias (in fact, they’re still called aliases in the code) that have been around for about 10–15 years. They contain the path plus a bunch more info. As macOS becomes more locked-down the recommended way of accessing files is to retrieve these bookmarks through the normal layers of system permissions and security. Without the bookmarks, for example just using plain text paths, I would not be able to show the full images in Quick Look or easily launch the list items. A key benefit is that the bookmark will still resolve if the file is moved somewhere else on the same disk, or even to a different volume!&lt;/p&gt;

&lt;p&gt;I store the items as JSON in the saved file, simply because I prefer it to XML (which is the main/default option). I wanted the files to still be human readable and editable to a degree.&lt;/p&gt;

&lt;p&gt;The files are launched using the default app specified by that file, so it can be changed on a per-file basis. Individual images might open in an image editor, image viewer, app to run OCR, script to run OCR on it, etc.&lt;/p&gt;

&lt;p&gt;Dealing with files was both cool and annoying, cool that you can get the bookmarks to files so easily, but annoying that you have to jump through so many hoops to work around the security and sandbox protections and end up having to do file requests in a long-winded way, and then there’s having to tweak plist entries to give the app just the right permissions. I’m using some features of SwiftUI that mean the app can’t run on anything before macOS 14 Sonoma. All-in-all I’d say modern macOS development is a bit of a mixed bag. Take it or leave it.&lt;/p&gt;

&lt;h2 id=&quot;bonus-tip&quot;&gt;Bonus tip&lt;/h2&gt;

&lt;p&gt;Make sure to &lt;em&gt;uncheck&lt;/em&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;System Settings&lt;/code&gt; &amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Desktop &amp;amp; Dock&lt;/code&gt; &amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Windows&lt;/code&gt; &amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Close windows when quitting an application&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then leave the windows of an app open as you quit it. When you next launch the app its windows will restore to their previous size and position. If you close the windows first, then the app will restore to having no windows open.&lt;/p&gt;

&lt;hr /&gt;

&lt;blockquote&gt;
  &lt;h2 id=&quot;thanks&quot;&gt;Thanks&lt;/h2&gt;

  &lt;p&gt;My good friend Dave Roberts (Serendipity App Company) for brainstorming why it was so damned difficult to get the app to react differently when a document was opened from Finder. Rip it up and start again! To Dustin Mierau for kickstarting this trend of remaking forgotten old apps for modern macOS. And eternal thanks to &lt;a href=&quot;https://twitter.com/chrispatterson/status/1822396663425532259&quot;&gt;Chris Patterson&lt;/a&gt; and Ali Rantakari for their apps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;since-launch&quot;&gt;Since launch&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;support added for macOS 12 and newer (was originally macOS 14 and newer)&lt;/li&gt;
  &lt;li&gt;new features added&lt;/li&gt;
  &lt;li&gt;omissions addressed&lt;/li&gt;
  &lt;li&gt;bug fixes&lt;/li&gt;
  &lt;li&gt;new icon on the way&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;elsewhere&quot;&gt;Elsewhere&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=41212193&quot;&gt;Hacker News&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://tidbits.com/2024/09/02/appbits-stapler-opens-multiple-apps-documents-and-folders/&quot;&gt;TidBits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Sat, 10 Aug 2024 21:00:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2024/08/10/stapler-i-remade-a-32-year-old-classic-macintosh-app/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2024/08/10/stapler-i-remade-a-32-year-old-classic-macintosh-app/</guid>
        </item>
      
    
      
        <item>
          <title>Using game controllers and keyboards for custom shortcuts</title>
          <description>&lt;p&gt;Recently I’ve been following a trend in &lt;a href=&quot;https://www.thetechedvocate.org/what-is-a-macro-pad-and-what-do-you-use-it-for/&quot;&gt;macro-pads&lt;/a&gt;, specialised/bespoke keyboards that provide an easy way to trigger keyboard shortcuts. A host of small companies have flooded the market with modified Bluetooth numeric pads that target Procreate, and Figma have teamed up with Work Louder to create a branded keypad with additional jog and rotary dials.&lt;/p&gt;

&lt;p&gt;But none of these seem quite right for me. I have so many controllers and devices already it felt better to make use of what I have to hand.&lt;/p&gt;

&lt;p&gt;I’m encouraged to find that there are lots of options! I’m a macOS user and I also dabble with Windows on occasion, but I have no idea how this can be done well on Linux.&lt;/p&gt;

&lt;p class=&quot;tofigure&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/8bitdo-micro-shortcuts.jpg&quot; alt=&quot;8Bitdo Micro&quot; title=&quot;&amp;lt;em&amp;gt;8Bitdo Micro&amp;lt;/em&amp;gt; is marketed as a multi-tasking controller&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;controllers&quot;&gt;Controllers&lt;/h2&gt;

&lt;p&gt;An obvious choice for a device with multiple buttons is a game controller. In modern macOS it’s easy to pair Nintendo Switch controllers, and the JoyCon (left or right) is an ideal candidate for a hand-held shortcut device. Xbox and PlayStation controllers can also be paired but they are much larger. Wired or wireless controllers will work.&lt;/p&gt;

&lt;p&gt;You can even use a Wii remote using an adapter like the &lt;a href=&quot;https://www.mayflash.com/product/magic_ns_lite.html&quot;&gt;&lt;em&gt;Mayflash MAGIC-NS Lite&lt;/em&gt;&lt;/a&gt;. Or you might use more esoteric controllers with an adapter from Robert Dale Smith’s &lt;a href=&quot;https://controlleradapter.com&quot;&gt;Controller Adapter&lt;/a&gt; store. In fact, I use one of his adapters to get an old &lt;a href=&quot;https://x.com/gingerbeardman/status/1629936413801062403?s=20&quot;&gt;&lt;em&gt;Sony Jog Controller&lt;/em&gt;&lt;/a&gt; to act like a GameCube controller, which I then map to keyboard shortcuts using the methods below. The sky’s the limit!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;8Bitdo&lt;/em&gt; also have their &lt;a href=&quot;https://www.8bitdo.com/micro/&quot;&gt;&lt;em&gt;Micro&lt;/em&gt; controller&lt;/a&gt; which offers more buttons and slightly better one-handed ergonomics than a Switch JoyCon. This controller is really interesting as it can pose as a Switch Pro Controller, generic controller, or keyboard. This gives us even more options. 8Bitdo are aware of this market and have created this device to fit, including a bespoke mobile app, and they even go so far as to &lt;a href=&quot;https://www.8bitdo.com/micro/#content-1-9&quot;&gt;call out this use case in their marketing material&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;keyboards&quot;&gt;Keyboards&lt;/h2&gt;

&lt;p&gt;These could be cheap bluetooth numeric pads or other small keyboards. I’ve personally tried a &lt;a href=&quot;https://niwanetwork.org/wiki/Nintendo_Wireless_Keyboard&quot;&gt;&lt;em&gt;Nintendo Wireless Keyboard&lt;/em&gt;&lt;/a&gt; that came with Nintendo DS game &lt;em&gt;Pokémon Typing Adventure&lt;/em&gt;. And of course the 8Bitdo controller mentioned above has a keyboard mode. There are many macro-pads listed for sale online, with various numbers of keys and rotary dials. Wired or wireless keyboards will work, so get creative!&lt;/p&gt;

&lt;h2 id=&quot;remapping&quot;&gt;Remapping&lt;/h2&gt;

&lt;p&gt;The 8Bitdo controller has a specific app for iOS/Android that can change its keyboard mappings. That’s cool for those mobile devices, and the iOS version will even run on an M1 Mac. But we can achieve a more versatile solution by doing the remapping in software on the computer.&lt;/p&gt;

&lt;p&gt;The general concept is to use an app that will take an input from your device of choice and map it to a keyboard shortcut or some other action that you specify. There are many apps that do this, so I’ve limited my list below to those that offer one important feature: &lt;em&gt;per-app mapping&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This means you can set different shortcuts for each app you use, and they will change as you move between apps. Of course, it is best if this happens automatically to prevent any friction in the process.&lt;/p&gt;

&lt;p&gt;I might have a button set to “zoom in” or “zoom out” that will trigger slightly different shortcuts in my text editor than in my image editor, or I might have a button set to “primary tool” and it will trigger a tool in a different way depending on the design app I am currently using.&lt;/p&gt;

&lt;p&gt;My personal favourite app is &lt;a href=&quot;https://github.com/qibinc/JoyMapperSilicon&quot;&gt;JoyMapperSilicon&lt;/a&gt;, but we really are spoiled for choice.&lt;/p&gt;

&lt;h2 id=&quot;mouse&quot;&gt;Mouse&lt;/h2&gt;

&lt;p&gt;It’s also possible to use the analog sticks and gyro of a Switch JoyCon as a mouse using QJoyControl. Analog is straightforward and as you’d expect. But using gyro basically gives you an Air Mouse which is pretty amazing.&lt;/p&gt;

&lt;h2 id=&quot;apps&quot;&gt;Apps&lt;/h2&gt;

&lt;p&gt;You can sort the table by headings: name, cost, platform, how the per-app function works, relative ease-of-use, and whether it works with devices keyboard (key) or controller (joy) or both.&lt;/p&gt;

&lt;div class=&quot;table-wrapper&quot;&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;Name&lt;/th&gt;
        &lt;th&gt;Cost&lt;/th&gt;
        &lt;th&gt;Platform&lt;/th&gt;
        &lt;th&gt;Per‑app?&lt;/th&gt;
        &lt;th&gt;Easy?&lt;/th&gt;
        &lt;th&gt;Type&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://support.8bitdo.com/ultimate/micro.html&quot;&gt;8BitDo Ultimate Software&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;Free&lt;/td&gt;
        &lt;td&gt;iOS/Android&lt;/td&gt;
        &lt;td&gt;Manual&lt;/td&gt;
        &lt;td&gt;Yes&lt;/td&gt;
        &lt;td&gt;Key&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://www.autohotkey.com&quot;&gt;AutoHotKey&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;Free&lt;/td&gt;
        &lt;td&gt;Windows&lt;/td&gt;
        &lt;td&gt;Auto&lt;/td&gt;
        &lt;td&gt;No&lt;/td&gt;
        &lt;td&gt;Joy&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://folivora.ai&quot;&gt;BetterTouchTool&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;$10&lt;/td&gt;
        &lt;td&gt;macOS&lt;/td&gt;
        &lt;td&gt;Auto&lt;/td&gt;
        &lt;td&gt;Yes&lt;/td&gt;
        &lt;td&gt;Both&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://apps.apple.com/gb/app/gamepad-companion/id428799479?mt=12&quot;&gt;GamePad Companion&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;$10&lt;/td&gt;
        &lt;td&gt;macOS&lt;/td&gt;
        &lt;td&gt;Manual&lt;/td&gt;
        &lt;td&gt;No&lt;/td&gt;
        &lt;td&gt;Joy&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://apps.apple.com/gb/app/joykeymapper/id1511416593?mt=12&quot;&gt;JoyKeyMapper&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;Free&lt;/td&gt;
        &lt;td&gt;macOS&lt;/td&gt;
        &lt;td&gt;Auto&lt;/td&gt;
        &lt;td&gt;Yes&lt;/td&gt;
        &lt;td&gt;Joy&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/qibinc/JoyMapperSilicon&quot;&gt;JoyMapperSilicon&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;Free&lt;/td&gt;
        &lt;td&gt;macOS&lt;/td&gt;
        &lt;td&gt;Auto&lt;/td&gt;
        &lt;td&gt;Yes&lt;/td&gt;
        &lt;td&gt;Joy&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://joytokey.net/en/&quot;&gt;JoyToKey&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;$7&lt;/td&gt;
        &lt;td&gt;Windows&lt;/td&gt;
        &lt;td&gt;Auto&lt;/td&gt;
        &lt;td&gt;No&lt;/td&gt;
        &lt;td&gt;Joy&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/zenangst/KeyboardCowboy&quot;&gt;Keyboard Cowboy&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;Free&lt;/td&gt;
        &lt;td&gt;macOS&lt;/td&gt;
        &lt;td&gt;Auto&lt;/td&gt;
        &lt;td&gt;No&lt;/td&gt;
        &lt;td&gt;Key&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://www.keyboardmaestro.com&quot;&gt;Keyboard Maestro&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;$36&lt;/td&gt;
        &lt;td&gt;macOS&lt;/td&gt;
        &lt;td&gt;Auto&lt;/td&gt;
        &lt;td&gt;No&lt;/td&gt;
        &lt;td&gt;Both&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/erikmwerner/QJoyControl&quot;&gt;QJoyControl&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;Free&lt;/td&gt;
        &lt;td&gt;macOS&lt;/td&gt;
        &lt;td&gt;No&lt;/td&gt;
        &lt;td&gt;Yes&lt;/td&gt;
        &lt;td&gt;Joy&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://www.usboverdrive.com&quot;&gt;USB Overdrive&lt;/a&gt;&lt;/td&gt;
        &lt;td&gt;$20&lt;/td&gt;
        &lt;td&gt;macOS&lt;/td&gt;
        &lt;td&gt;Auto&lt;/td&gt;
        &lt;td&gt;Yes&lt;/td&gt;
        &lt;td&gt;Both&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

&lt;/div&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Sat, 23 Dec 2023 23:06:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2023/12/23/using-game-controllers-and-keyboards-for-custom-shortcuts/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2023/12/23/using-game-controllers-and-keyboards-for-custom-shortcuts/</guid>
        </item>
      
    
      
        <item>
          <title>OpenSCAD to Sprite Sheet workflow</title>
          <description>&lt;p&gt;I just released the “OpenSCAD to Spritesheet” workflow I created for &lt;a href=&quot;/tag/dailydriver/&quot;&gt;Daily Driver&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/gingerbeardman/openscad-spritesheet&quot;&gt;github.com/gingerbeardman/openscad-spritesheet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a Frankenstein mish-mash of a Makefile and several shell scripts that evolved over many months/years. Initial rendering is done using OpenSCAD, and post-processing is done using ImageMagick. Model poses and rendering variations are controlled by variables in either the shell script or passed through to the model. The whole process is optimised to do as much in parallel as I could figure. More info at the link above! 🚗💨&lt;/p&gt;

&lt;h2 id=&quot;post-processing&quot;&gt;Post Processing&lt;/h2&gt;

&lt;p&gt;After exporting all frames there is some &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;image magick&lt;/code&gt; work to process the files as follows:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;stitch frames together into a single sprite sheet&lt;/li&gt;
  &lt;li&gt;split sprite sheet into RGBA channels&lt;/li&gt;
  &lt;li&gt;process channels to recolour and dither as required&lt;/li&gt;
  &lt;li&gt;recombine processed channels into new sprite sheet image&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can read about that in a &lt;a href=&quot;/2021/06/05/channelling-rgb-into-1bit/&quot;&gt;previous blog post&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;benchmarks&quot;&gt;Benchmarks&lt;/h2&gt;

&lt;p&gt;A full build of 36 cars is as follows:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;3GHz 6-core Intel Mac mini 32GB
    &lt;ul&gt;
      &lt;li&gt;100% CPU for ~26 minutes&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;M1 Pro 10-core MacBook Pro 16GB
    &lt;ul&gt;
      &lt;li&gt;70% CPU for ~9 mins&lt;/li&gt;
      &lt;li&gt;about 3x speedup&lt;/li&gt;
      &lt;li&gt;approx 16 seconds per car&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s parallel 3D rendering, PNG writing &amp;amp; compositing &amp;amp; processing, and copying of ~140K files (which takes up ~0.5GB of disk space).&lt;/p&gt;

&lt;h2 id=&quot;example-model&quot;&gt;Example Model&lt;/h2&gt;

&lt;p&gt;Not to scale! Sizes of features are exagerated to allow for them to appear correct when rendered at a very small size.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/openscad-spritesheet-model-car.png&quot; alt=&quot;PNG&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;example-output&quot;&gt;Example Output&lt;/h2&gt;

&lt;p&gt;990 frames each for car and shadow, total of 1980 frames per sprite sheet. Each sprite sheet takes up about ~400KB of RAM on Playdate, and only one is loaded at a time.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/posts/openscad-spritesheet-car-table-38-38.png&quot;&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/openscad-spritesheet-car-table-38-38.png&quot; alt=&quot;PNG&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Wed, 04 Oct 2023 15:19:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2023/10/04/openscad-to-sprite-sheet/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2023/10/04/openscad-to-sprite-sheet/</guid>
        </item>
      
    
      
        <item>
          <title>Daily Driver: Club GTi</title>
          <description>&lt;p&gt;My &lt;a href=&quot;/2023/06/07/gti-cub-supermini-festa/&quot;&gt;GTi Club: Supermini Festa!&lt;/a&gt; obsession has bled into my game &lt;a href=&quot;/tag/dailydriver/&quot;&gt;Daily Driver&lt;/a&gt;… the “Club GTi” car features smaller wheels than the standard size, more roll during turning, and lots of detail and decoration to show off the rendering workflow I spent so many hours on. A good choice for the autotesting/motorkhana/gymkhana stages.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/daily-driver-club-gti.gif#playdate&quot; alt=&quot;GIF&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Here’s the 3D model, created in &lt;a href=&quot;https://openscad.org&quot;&gt;OpenSCAD&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/daily-driver-club-gti-model.png&quot; alt=&quot;PNG&quot; /&gt;&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Tue, 08 Mar 2022 12:32:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2022/03/08/club-gti/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2022/03/08/club-gti/</guid>
        </item>
      
    
      
        <item>
          <title>Daily Driver: Makefile</title>
          <description>&lt;p&gt;During the shadow rendering changes I was generating new sprites quite frequently, and I quickly realised that my mess of shell scripts just wasn’t a good enough system to be used in anger. I would have to call my scripts manually, with a variety of parameters, and remember a bunch of stuff unique to each script.&lt;/p&gt;

&lt;p&gt;So I spent a day converting the script system into a Makefile, with rules for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;rendering&lt;/li&gt;
  &lt;li&gt;stitching&lt;/li&gt;
  &lt;li&gt;post-processing&lt;/li&gt;
  &lt;li&gt;copying&lt;/li&gt;
  &lt;li&gt;cleaning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also improved the scripts to make them touch all their output files with the last-modified timestamp of the original 3D model, meaning the scripts could become self-aware and know which output files are up-to-date and which need re-rendering.&lt;/p&gt;

&lt;p&gt;With this system I can generate a full set of renders of all vehicles in ~minutes, or render just a single car in ~seconds. This is such a huge time saver and has enabled me to iterate on car designs more quickly and easily than ever before. I’m already seeing the gains in much better sprites.&lt;/p&gt;

&lt;p&gt;Believe it or not this was the first Makefile I’ve ever needed to write from scratch. I knew enough about them to know that they would be a good fit for this task, and it was fun to learn more about something I only knew in passing.&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Wed, 19 May 2021 00:00:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2021/05/19/makefile/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2021/05/19/makefile/</guid>
        </item>
      
    
      
        <item>
          <title>Daily Driver: Automation Improvements</title>
          <description>&lt;p&gt;The recent automation was really just help with organisation. As soon as I started looking at running OpenSCAD from the shell/command-line it became obvious that I could do the rendering and organisation in one step without having to use external apps like Hazel.&lt;/p&gt;

&lt;p&gt;So, that’s now done. I render all the frames, with more sensible filenames, to a single folder.&lt;/p&gt;

&lt;p&gt;If I run all the renders one after the other, maxing out a single CPU core (99% CPU usage), time taken:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;~17 seconds 🐢&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But, using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&lt;/code&gt; directive and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wait&lt;/code&gt; command, I can run the renders in parallel (well, technically it’s one process each; and batches of 32 works best) using all 6 CPU cores (~485% CPU usage), time taken:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;~10 seconds 🐇&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;…the beauty of the command line!&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note to self: don’t publish a post about an automation breakthrough until the dust has settled.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Fri, 09 Oct 2020 00:00:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2020/10/09/automation-improvements/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2020/10/09/automation-improvements/</guid>
        </item>
      
    
      
        <item>
          <title>Daily Driver: Automation</title>
          <description>&lt;p&gt;In preparation for regenerating my many cars with x3 the number of sprites, I thought I’d try to sort the rendered frames automatically into named folders because this is fiddly manual work I really don’t enjoy, and a bit of a bottleneck in my asset generation. For each pose I have to render the frames then group the new image files into a folder that describes that pose, as these multiple folders are later be used for batch processing.&lt;/p&gt;

&lt;p&gt;I could use macOS Folder Actions for this, but I’ve been using an app called &lt;a href=&quot;https://www.noodlesoft.com/&quot;&gt;Hazel&lt;/a&gt; for many years to do this sort of thing, so that was my first choice.&lt;/p&gt;

&lt;p&gt;The hard work is done with a shell script, as I’m quite comfortable writing those.&lt;/p&gt;

&lt;h2 id=&quot;flow&quot;&gt;Flow&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;get most recently saved .scad file&lt;/li&gt;
  &lt;li&gt;parse filename to capture car name&lt;/li&gt;
  &lt;li&gt;parse file contents for left/right/forward/backward tilt values&lt;/li&gt;
  &lt;li&gt;concatenate all this information as our new folder name&lt;/li&gt;
  &lt;li&gt;create new folder&lt;/li&gt;
  &lt;li&gt;move matching file into new folder&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means that the folder name will change based on the render settings in the file—perfect!&lt;/p&gt;

&lt;p&gt;I still have to make 9 small sets of manual text changes to render each car pose, so that’s the next thing I’ll try to automate by running OpenSCAD from the command line.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://cdn.gingerbeardman.com/images/posts/daily-driver-automation.png&quot; alt=&quot;PNG&quot; /&gt;&lt;/p&gt;
</description>
          <author>by Matt Sephton</author>
          <pubDate>Wed, 07 Oct 2020 00:00:00 +0000</pubDate>
          <link>https://blog.gingerbeardman.com/2020/10/07/automation/</link>
          <guid isPermaLink="true">https://blog.gingerbeardman.com/2020/10/07/automation/</guid>
        </item>
      
    

  </channel>
</rss>
