<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
    <channel>
        <title>notes: posts</title>
        <link>https://notes.zachmanson.com/posts</link>
        <description>Notes tagged #posts</description>
        <atom:link href="https://notes.zachmanson.com/posts" rel="self" />
        <docs>http://www.rssboard.org/rss-specification</docs>
        <generator>ochrs</generator>
        <image>
            <url>https://zachmanson.com/icons/android-chrome-256x256.png</url>
            <title>notes: posts</title>
            <link>https://notes.zachmanson.com/posts</link>
        </image>
        <language>en</language>
        <lastBuildDate>Sat, 09 May 2026 04:04:15 </lastBuildDate>
        
        <item>
            <title>Pirate Libraries</title>
            <link>https://notes.zachmanson.com/pirate-libraries</link>
            
            <content:encoded>
                <![CDATA[<p>Also known as shadow libraries, are potentially the best thing humans have ever created and are illegal in most places.  They are <a href="https://notes.zachmanson.com/archives">archives</a> human <a href="https://notes.zachmanson.com/on-writing">writing</a>, some catering to particular niches and some attempting to capture <em>everything</em>.  And then distribute all of human knowledge and writing for free.</p>
<p>They have no regard for copyright law, generally viewing them as part of a broken system or distractions to (what the pirate librarians might call) their noble goals.</p>
<p>Library Genesis (libgen), Sci-Hub (a superset of libgen) and annas-archive focus primarily on academic materials, textbooks and journal publications.  The projects are borne from the goal of allowing access to documents that would normally only be accessible through expensive journals.</p>
<p>Books3 is another pirate library, containing ≈196000 books sourced from Bibliotik.  It is named in the spirit of OpenAI's dubiously sourced collections of books, Books1 and Books2.  Books3 has become well known for its use in training <a href="https://notes.zachmanson.com/large-language-models">large language models</a> like <a href="https://notes.zachmanson.com/llama">LLaMA</a>.</p>
<p>The <a href="https://notes.zachmanson.com/internet">Internet</a> Archive is pirate library adjacent.  It contains and redistributes (without permission) countless copyrighted works, but will respond to takedown requests.</p>
<p>During <a href="https://notes.zachmanson.com/uwa-computer-science">my time at university</a>, use of Library Genesis and Sci-Hub was rife. While not everyone knew of these sites directly, everyone knew someone who knew someone who did, and textbook PDFs propagated on the sneakernet.</p>
<p>Pirate libraries:</p>
<ul>
<li>Library Genesis</li>
<li>Sci-Hub</li>
<li>Bibliotik</li>
<li>Books3</li>
<li>Gigapedia</li>
<li>Kolkhoz</li>
<li>Librusec</li>
<li>Anna's Archive</li>
</ul>
<p>An adjacent concept is preservation of media, where pirated copies are seen as a defence against permanent loss. This is the thinking behind the Internet Archive and the sites it has inspired.  This kind of thinking is also prevalent in the world of video games, a medium where many works are rendered unavailable by missing online services, changing hardware landscape or lost source code. There are many cases where old games have been recovered or restored through pirated versions.</p>
<p>Web archives:</p>
<ul>
<li><a href="https://archive.org">Internet Archive</a></li>
<li><a href="https://archive.is">archive.is</a></li>
<li><a href="https://archive.today">archive.today</a></li>
</ul>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://www.atlasobscura.com/articles/the-rise-of-illegal-pirate-libraries">The Rise of Pirate Libraries</a></li>
<li><a href="https://www.wired.com/story/battle-over-books3/">The Battle Over Books3</a></li>
<li><a href="https://reason.com/2022/07/24/you-cant-stop-pirate-libraries/">You Can't Stop Pirate Libraries</a></li>
<li><a href="https://publiclibrariesonline.org/2016/06/the-pirate-library-controversy/">The Pirate Library Controversy</a></li>
<li><a href="https://lostmediawiki.com/Home">Lost Media Wiki</a></li>
<li><a href="https://www.theatlantic.com/technology/archive/2017/04/the-tragedy-of-google-books/523320/">Torching the Modern-Day Library of Alexandria</a> (<a href="https://archive.is/rQ7Zb">archive.is</a>)</li>
<li><a href="https://annas-archive.org/blog/all-isbns.html">Visualising all ISBNs</a><ul>
<li><a href="https://phiresky.github.io/blog/2025/visualizing-all-books-in-isbn-space/">And a solution</a></li>
</ul>
</li>
</ul>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/pirate-libraries</guid>
            <pubDate>2023-11-20</pubDate>
        </item>
        
        <item>
            <title>Git Activity in Multiple Folders</title>
            <link>https://notes.zachmanson.com/git-activity-in-multiple-folders</link>
            
            <description>A template for listing all the commits you did on a certain date.</description>
            
            <content:encoded>
                <![CDATA[<p>A script I like to drop into my projects folders to see what I was working on for a given day.</p>
<div class="highlight"><pre><span></span><code><span class="nv">day</span><span class="o">=</span><span class="nv">$1</span>
<span class="nv">repo_folder</span><span class="o">=</span>~/projects
<span class="k">for</span><span class="w"> </span>dir<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$repo_folder</span><span class="s2">&quot;</span>/*/<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">    </span><span class="c1"># Check if the directory is a git repository</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-d<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$dir</span><span class="s2">/.git&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">        </span><span class="nb">cd</span><span class="w"> </span><span class="nv">$dir</span>
<span class="w">        </span>git<span class="w"> </span>log<span class="w"> </span>--after<span class="o">=</span><span class="s2">&quot;</span><span class="nv">$day</span><span class="s2"> 00:00&quot;</span><span class="w"> </span>--before<span class="o">=</span><span class="s2">&quot;</span><span class="nv">$day</span><span class="s2"> 23:59?&quot;</span><span class="w"> </span>--author<span class="o">=</span><span class="s2">&quot;Zach&quot;</span>
<span class="w">    </span><span class="k">fi</span>
<span class="k">done</span>
</code></pre></div>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/git-activity-in-multiple-folders</guid>
            <pubDate>2024-04-22</pubDate>
        </item>
        
        <item>
            <title>Feed URI Scheme</title>
            <link>https://notes.zachmanson.com/feed-uri-scheme</link>
            
            <description>An internet that could have been.</description>
            
            <content:encoded>
                <![CDATA[<p>A Wikipedia article that makes me sad is the one on <a href="https://en.wikipedia.org/wiki/Feed_URI_scheme">Feed URI scheme</a>. It is a draft proposal from 2003 for a URI scheme designed to link to <a href="https://notes.zachmanson.com/rss">RSS</a>/Atom Readers directly to a feed. </p>
<p>A link that looks like this <a href="feed://notes.zachmanson.com/posts.xml"><code>feed://notes.zachmanson.com/posts.xml</code></a> can be clicked on and will open directly a feed aggregator, no matter the origin of the link. This is essentially a <strong>subscribe button for the whole internet</strong>. </p>
<p>It never made it to official status, and the Wikipedia page listing programs that support it was last updated in 2006. The software cited is NetNewsWire, FeedDemon, Flock, and Safari(‽‽‽‽). Of these, FeedDemon was discontinued in June 2013, Flock was a browser that was discontinued in 2011, Safari support was removed at some point after 2007<sup id="fnref:1"><a class="footnote-ref" href="#fn:1">1</a></sup><sup id="fnref:2"><a class="footnote-ref" href="#fn:2">2</a></sup>. It also seems that it was tested in Outlook 2007<sup id="fnref:4"><a class="footnote-ref" href="#fn:4">3</a></sup>, but I am unsure if this shipped.</p>
<p>NetNewsWire still exists, is actively supported and is extremely high quality. At the time of writing it is what I use for following feeds. 19 years later, it still supports <code>feed://</code> links! It works great. I wish this was more common.</p>
<p>I discovered that Pocket Casts, my longtime podcast app<sup id="fnref:3"><a class="footnote-ref" href="#fn:3">4</a></sup> also supports <code>feed://</code> links, interpreting the link as a podcast feed, though it seemed unable to differentiate podcast feeds from text feeds.</p>
<p>In honour of this I have added a subscribe link to this website!</p>
<div class="footnote">
<hr />
<ol>
<li id="fn:1">
<p><a href="https://lobste.rs/c/d29mru">As stated by Jens Alfke</a>, a former Apple developer, who developed the feature with Ricci Adams. The comment also mentions that the feed reader was supported in Apple Mail, although I do not know if Apple Mail supported <code>feed://</code> links.&#160;<a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text">&#8617;</a></p>
</li>
<li id="fn:2">
<p><a href="https://discussions.apple.com/thread/5175501">The Apple Mail RSS support was removed in Mountain Lion</a>&#160;<a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text">&#8617;</a></p>
</li>
<li id="fn:4">
<p><a href="https://web.archive.org/web/20211124234409/https://www.hanselman.com/blog/ie-7-office-2007-rss-and-the-feed-protocol">Outlook 2007 Beta 2 registered</a> itself as a system-wide handler for the schema&#160;<a class="footnote-backref" href="#fnref:4" title="Jump back to footnote 3 in the text">&#8617;</a></p>
</li>
<li id="fn:3">
<p>I purchased Pocket Casts in 2016. They have gone through a few owners and a few pricing structure changes in that time. In my 9 years of using it I've never seen an ad, until last week. Comments by Matt Mullenweg on HN imply this might be erroneous, but I have my doubts. If the ads persist, I will find a fork or create my own.&#160;<a class="footnote-backref" href="#fnref:3" title="Jump back to footnote 4 in the text">&#8617;</a></p>
</li>
</ol>
</div>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/feed-uri-scheme</guid>
            <pubDate>2025-10-02</pubDate>
        </item>
        
        <item>
            <title>Next.js with tRPC and Layouts</title>
            <link>https://notes.zachmanson.com/next.js-with-trpc-and-layouts</link>
            
            <content:encoded>
                <![CDATA[<p>An interesting type error arises if you use the standard advice for installing tRPC on an existing Next.js project that already has a layout applied.</p>
<p>Normally the advice for creating an app with a layout is to do this in your <code>src/pages/_app.tsx</code>:</p>
<div class="highlight"><pre><span></span><code><span class="c1">// src/pages/_app.tsx</span>

<span class="c1">// ... imports go here</span>

<span class="k">export</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="nx">NextPageWithLayout</span><span class="o">&lt;</span><span class="nx">P</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{},</span><span class="w"> </span><span class="nx">IP</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">P</span><span class="o">&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">NextPage</span><span class="o">&lt;</span><span class="nx">P</span><span class="p">,</span><span class="w"> </span><span class="nx">IP</span><span class="o">&gt;</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">getLayout</span><span class="o">?:</span><span class="w"> </span><span class="p">(</span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">ReactElement</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">ReactNode</span><span class="p">;</span>
<span class="p">};</span>

<span class="kr">type</span><span class="w"> </span><span class="nx">AppPropsWithLayout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">AppProps</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">Component</span><span class="o">:</span><span class="w"> </span><span class="kt">NextPageWithLayout</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">function</span><span class="w"> </span><span class="nx">App</span><span class="p">({</span>
<span class="w">  </span><span class="nx">Component</span><span class="p">,</span>
<span class="w">  </span><span class="nx">pageProps</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">session</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">pageProps</span><span class="w"> </span><span class="p">},</span>
<span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="nx">AppPropsWithLayout</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">getLayout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Component</span><span class="p">.</span><span class="nx">getLayout</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="p">((</span><span class="nx">page</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">page</span><span class="p">);</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="nx">getLayout</span><span class="p">(</span>
<span class="w">    </span><span class="o">&lt;</span><span class="nx">SessionProvider</span><span class="w"> </span><span class="nx">session</span><span class="o">=</span><span class="p">{</span><span class="nx">session</span><span class="p">}</span><span class="o">&gt;</span>
<span class="w">      </span><span class="o">&lt;</span><span class="nx">GlobalProvider</span><span class="o">&gt;</span>
<span class="w">        </span><span class="o">&lt;</span><span class="nx">Layout</span><span class="o">&gt;</span>
<span class="w">          </span><span class="o">&lt;</span><span class="nx">Component</span><span class="w"> </span><span class="p">{...</span><span class="nx">pageProps</span><span class="p">}</span><span class="w"> </span><span class="o">/&gt;</span>
<span class="w">        </span><span class="o">&lt;</span><span class="err">/Layout&gt;</span>
<span class="w">      </span><span class="o">&lt;</span><span class="err">/GlobalProvider&gt;</span>
<span class="w">    </span><span class="o">&lt;</span><span class="err">/SessionProvider&gt;</span>
<span class="w">  </span><span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">App</span>
</code></pre></div>
<p>This works pretty well.</p>
<p>The docs for tRPC suggest wrapping that export:</p>
<div class="highlight"><pre><span></span><code><span class="c1">// src/pages/_app.tsx</span>
<span class="k">import</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">AppType</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;next/app&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">trpc</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;../utils/trpc&#39;</span><span class="p">;</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">MyApp</span><span class="o">:</span><span class="w"> </span><span class="kt">AppType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">Component</span><span class="p">,</span><span class="w"> </span><span class="nx">pageProps</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="o">&lt;</span><span class="nx">Component</span><span class="w"> </span><span class="p">{...</span><span class="nx">pageProps</span><span class="p">}</span><span class="w"> </span><span class="o">/&gt;</span><span class="p">;</span>
<span class="p">};</span>
<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">trpc</span><span class="p">.</span><span class="nx">withTRPC</span><span class="p">(</span><span class="nx">MyApp</span><span class="p">);</span>
</code></pre></div>
<p>Combining these seems simple:</p>
<div class="highlight"><pre><span></span><code><span class="c1">// src/pages/_app.tsx</span>

<span class="c1">// ... imports go here</span>

<span class="k">export</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="nx">NextPageWithLayout</span><span class="o">&lt;</span><span class="nx">P</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{},</span><span class="w"> </span><span class="nx">IP</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">P</span><span class="o">&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">NextPage</span><span class="o">&lt;</span><span class="nx">P</span><span class="p">,</span><span class="w"> </span><span class="nx">IP</span><span class="o">&gt;</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">getLayout</span><span class="o">?:</span><span class="w"> </span><span class="p">(</span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">ReactElement</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">ReactNode</span><span class="p">;</span>
<span class="p">};</span>

<span class="kr">type</span><span class="w"> </span><span class="nx">AppPropsWithLayout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">AppProps</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">Component</span><span class="o">:</span><span class="w"> </span><span class="kt">NextPageWithLayout</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">function</span><span class="w"> </span><span class="nx">App</span><span class="p">({</span>
<span class="w">  </span><span class="nx">Component</span><span class="p">,</span>
<span class="w">  </span><span class="nx">pageProps</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">session</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">pageProps</span><span class="w"> </span><span class="p">},</span>
<span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="nx">AppPropsWithLayout</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">getLayout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Component</span><span class="p">.</span><span class="nx">getLayout</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="p">((</span><span class="nx">page</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">page</span><span class="p">);</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="nx">getLayout</span><span class="p">(</span>
<span class="w">    </span><span class="o">&lt;</span><span class="nx">SessionProvider</span><span class="w"> </span><span class="nx">session</span><span class="o">=</span><span class="p">{</span><span class="nx">session</span><span class="p">}</span><span class="o">&gt;</span>
<span class="w">      </span><span class="o">&lt;</span><span class="nx">GlobalProvider</span><span class="o">&gt;</span>
<span class="w">        </span><span class="o">&lt;</span><span class="nx">Layout</span><span class="o">&gt;</span>
<span class="w">          </span><span class="o">&lt;</span><span class="nx">Component</span><span class="w"> </span><span class="p">{...</span><span class="nx">pageProps</span><span class="p">}</span><span class="w"> </span><span class="o">/&gt;</span>
<span class="w">        </span><span class="o">&lt;</span><span class="err">/Layout&gt;</span>
<span class="w">      </span><span class="o">&lt;</span><span class="err">/GlobalProvider&gt;</span>
<span class="w">    </span><span class="o">&lt;</span><span class="err">/SessionProvider&gt;</span>
<span class="w">  </span><span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">trpc</span><span class="p">.</span><span class="nx">withTRPC</span><span class="p">(</span><span class="nx">App</span><span class="p">)</span>
</code></pre></div>
<p>But this will throw a type error</p>
<p><img alt="" src="https://notes.zachmanson.com/media/trpc-typeerror.png" /></p>
<div class="highlight"><pre><span></span><code><span class="nx">Argument</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="s1">&#39;({ Component, pageProps }: AppPropsWithLayout) =&gt; ReactNode&#39;</span><span class="w"> </span><span class="nx">is</span><span class="w"> </span><span class="nx">not</span><span class="w"> </span><span class="nx">assignable</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">parameter</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="s1">&#39;NextComponentType&lt;any, any, any&gt;&#39;</span><span class="p">.</span>
<span class="w">  </span><span class="nx">Type</span><span class="w"> </span><span class="s1">&#39;({ Component, pageProps }: AppPropsWithLayout) =&gt; ReactNode&#39;</span><span class="w"> </span><span class="nx">is</span><span class="w"> </span><span class="nx">not</span><span class="w"> </span><span class="nx">assignable</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="s1">&#39;FunctionComponent&lt;any&gt; &amp; { getInitialProps?(context: any): any; }&#39;</span><span class="p">.</span>
<span class="w">    </span><span class="nx">Type</span><span class="w"> </span><span class="s1">&#39;({ Component, pageProps }: AppPropsWithLayout) =&gt; ReactNode&#39;</span><span class="w"> </span><span class="nx">is</span><span class="w"> </span><span class="nx">not</span><span class="w"> </span><span class="nx">assignable</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="s1">&#39;FunctionComponent&lt;any&gt;&#39;</span><span class="p">.</span>
<span class="w">      </span><span class="nx">Type</span><span class="w"> </span><span class="s1">&#39;ReactNode&#39;</span><span class="w"> </span><span class="nx">is</span><span class="w"> </span><span class="nx">not</span><span class="w"> </span><span class="nx">assignable</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="s1">&#39;ReactElement&lt;any, any&gt; | null&#39;</span><span class="p">.</span>
<span class="w">        </span><span class="nx">Type</span><span class="w"> </span><span class="s1">&#39;undefined&#39;</span><span class="w"> </span><span class="nx">is</span><span class="w"> </span><span class="nx">not</span><span class="w"> </span><span class="nx">assignable</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="s1">&#39;ReactElement&lt;any, any&gt; | null&#39;</span><span class="p">.</span><span class="nx">ts</span><span class="p">(</span><span class="mf">2345</span><span class="p">)</span>
</code></pre></div>
<p>This is because <code>getLayout</code> returns a <code>ReactNode</code>,  the tRPC wrapper expects <code>NextComponentType</code>, which means it expects a <code>ReactElement</code> to be returned?</p>
<p>What's the difference between a <code>ReactNode</code> and a <code>ReactElement</code>?</p>
<p><a href="https://stackoverflow.com/a/58123882">This StackOverflow post</a> has a great explanation.</p>
<blockquote>
<p>A <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9f855c408dac3c7b3bf0ed9d78242ce073c7aaf1/types/react/index.d.ts#L327"><code>ReactElement</code></a> is an object with <code>type</code>, <code>props</code>, and <code>key</code> properties:<br />
...<br />
A <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9f855c408dac3c7b3bf0ed9d78242ce073c7aaf1/types/react/index.d.ts#L478"><code>ReactNode</code></a> is a <code>ReactElement</code>, <code>string</code>, <code>number</code>, <code>Iterable&lt;ReactNode&gt;</code>, <code>ReactPortal</code>, <code>boolean</code>, <code>null</code>, or <code>undefined</code>:</p>
</blockquote>
<p><cite class="standalone"><a href="https://stackoverflow.com/users/2326961/g%c3%a9ry-ogam">Géry Ogam</a></cite></p>
<p>So  <code>ReactElement</code>  can be cast to <code>ReactNode</code>, but not the other way around.</p>
<p>There is a simple fix.  If you move the get layout call inside another React component, it will go back to being a <code>ReactElement</code>.</p>
<div class="highlight"><pre><span></span><code><span class="kr">type</span><span class="w"> </span><span class="nx">AppPropsWithLayout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">AppProps</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">Component</span><span class="o">:</span><span class="w"> </span><span class="kt">NextPageWithLayout</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">function</span><span class="w"> </span><span class="nx">App</span><span class="p">({</span>
<span class="w">  </span><span class="nx">Component</span><span class="p">,</span>
<span class="w">  </span><span class="nx">pageProps</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">session</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">pageProps</span><span class="w"> </span><span class="p">},</span>
<span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="nx">AppPropsWithLayout</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">getLayout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Component</span><span class="p">.</span><span class="nx">getLayout</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="p">((</span><span class="nx">page</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">page</span><span class="p">);</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">layout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">getLayout</span><span class="p">(</span>
<span class="w">    </span><span class="o">&lt;</span><span class="nx">Layout</span><span class="o">&gt;</span>
<span class="w">      </span><span class="o">&lt;</span><span class="nx">Component</span><span class="w"> </span><span class="p">{...</span><span class="nx">pageProps</span><span class="p">}</span><span class="w"> </span><span class="o">/&gt;</span>
<span class="w">    </span><span class="o">&lt;</span><span class="err">/Layout&gt;</span>
<span class="w">  </span><span class="p">);</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">    </span><span class="o">&lt;</span><span class="nx">SessionProvider</span><span class="w"> </span><span class="nx">session</span><span class="o">=</span><span class="p">{</span><span class="nx">session</span><span class="p">}</span><span class="o">&gt;</span>
<span class="w">      </span><span class="o">&lt;</span><span class="nx">GlobalProvider</span><span class="o">&gt;</span>
<span class="w">        </span><span class="p">{</span><span class="nx">layout</span><span class="p">}</span>
<span class="w">      </span><span class="o">&lt;</span><span class="err">/GlobalProvider&gt;</span>
<span class="w">    </span><span class="o">&lt;</span><span class="err">/SessionProvider&gt;</span>
<span class="w">  </span><span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">trpc</span><span class="p">.</span><span class="nx">withTRPC</span><span class="p">(</span><span class="nx">App</span><span class="p">)</span>
</code></pre></div>
<p>Lucky I was able to find <a href="https://brockherion.dev/blog/posts/creating-per-page-layouts-with-nextjs-typescript-trcp-and-nextauth/">this tutorial</a> which mentions this exact footgun while I was working on adding tRPC to <a href="https://notes.zachmanson.com/penultimate-guitar">Penultimate Guitar</a>.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/next.js-with-trpc-and-layouts</guid>
            <pubDate>2024-07-04</pubDate>
        </item>
        
        <item>
            <title>The H Chord</title>
            <link>https://notes.zachmanson.com/the-h-chord</link>
            
            <description>How German monks broke my website</description>
            
            <content:encoded>
                <![CDATA[<p>I received a <a href="https://github.com/pavo-etc/penultimate-guitar/issues/41">strange bug report</a> on <a href="https://notes.zachmanson.com/penultimate-guitar">Penultimate Guitar</a> where the Next.js rendering would fail completely on certain songs.</p>
<h2 id="the-error">The Error</h2>
<p>The type error was being triggered by a chord component, where the key was undefined. The chords are pulled from Ultimate Guitar, so I inspected the JSON payload from the <a href="https://tabs.ultimate-guitar.com/tab/1684995">original source</a>.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/ug.png" /></p>
<p>Why the hell is there a <code>[ch]H[/ch]</code> chord in there? That's definitely the cause as I never accounted for non-existent keys. Looking at Ultimate Guitar, it renders as a B chord.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/ug-rendered.png" /></p>
<p>Huh?</p>
<details>
<summary>An aside on Ultimate Guitar HTML</summary>
<p>The way Ultimate Guitar handles data is bizarre. It passes a static dehydrated <a href="https://notes.zachmanson.com/html">HTML</a> page to the client.  The data payload is in the HTML as well, but instead of being contained in a <code>script</code> tag it's a giant JSON payload in an escaped string within an attribute of a random <code>div</code></p>
</details>
<h2 id="one-google-search-later">One Google Search Later</h2>
<p>Germany and the Netherlands ha(d/ve) their own musical key notation that include(d/s) a H chord. <a href="https://www.guitarsite.com/newsletters/010122/12.shtml">This site</a> claims it ended in "1994/1995", though I've seen <a href="https://github.com/pavo-etc/penultimate-guitar/issues/41#issuecomment-1538452351">other</a> <a href="https://www.reddit.com/r/musictheory/comments/8rn0ve">sources</a> claim its still taught this way. This comes as an artefact of the bizarre history Western music notation, which is a problem I seem to keep running into recently in my attempts to learn more about music theory.</p>
<blockquote>
<p>I learned in music school that the chord is called "B" (like in the rest of the world), but the note is called "H" (eg. the C Major scale would be C, D, E, F, G, A, H, C).</p>
</blockquote>
<p><cite>TobTobXX, who reported the bug</cite></p>
<h2 id="western-musics-stupid-origins">Western Music's Stupid Origins</h2>
<p>Western music is based on ecclesiastic modes used in church in the early Middle Ages, which only used the diatonic notes of the C scale (<em>natural</em> notes). The musical notation systems of the time reflected this, not accounting for notes outside of the C scale. When sharps and flats later came into more common use, the existing notation systems needed a way to distinguish them from the natural notes they sat between.</p>
<p>This problem first arose with B natural and B flat, according to the <a href="https://www.britannica.com/art/musical-expression">Encyclopedia Britannica</a>. The first method of distinguishing B from B flat was using two different forms of the lowercase "b" character:</p>
<p><img alt="" src="https://notes.zachmanson.com/media/The-Flat-Sharp-And-Natural-A-Historical-Sketch.png" /></p>
<p>Niecks, Frederick. “The Flat, Sharp, and Natural. A Historical Sketch.” <em>Proceedings of the Musical Association</em>, vol. 16, 1889, pp. 79–100.</p>
<p>Somewhere along the line in Germany, monks transcribing these square and round "b" characters confused the squared "b" for "h", and this was later assumed to be intentional. "H" became a convention for writing B natural, while the "b" character remained convention for writing B flat. "B" and "b" became amalgamated, both coming to represent modern B flat.</p>
<p>In the rest of the world, this "H" note didn't catch on.</p>
<p>In time, notation for sharp and flat notes other than B flat was needed. This use of square and round "b" to denote B and B flat eventually evolved into our modern notation for indicating natural and non-natural, <em>accidentals</em>. ♭ comes from the round "b", while ♯ and ♮ come from the square "b".</p>
<p>Somehow, Germany still hasn't fully corrected this mistake, continuing to use "H" to represent B natural in many places. Including Ultimate Guitar. Ugh.</p>
<h2 id="resolution">Resolution</h2>
<p>I don't love dealing with problems caused by the whims of millennia dead monks, but this was an interesting rabbit hole to fall into. The issue has since been patched, and I look forward to my mistakes ruining someone's day in 3023.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/gods-mistakes.png" /></p>
<details>
<summary>An aside on The Flat, Sharp, and Natural. A Historical Sketch</summary>
<p>When I first copied the text from the article, it copied that "square b" as a "h". Funny that modern OCR technology makes the same mistakes at 1000 year old monks.</p>
<blockquote>
<p>The first known writer who distinguished between b natural and b flat was Odo of Clugny, who died in 942 ; the b natural being indicated by a square b (h), the b flat by a round b (b)</p>
</blockquote>
<p>It was also a pain in the ass to find a copy of that article. It's mostly found on paywalled academic sites despite the article definitely being out of copyright. Luckily the Internet Archive <a href="https://scholar.archive.org/work/3jdud373effq3e376gqtlkxqvq">has a copy</a>.</p>
</details>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/the-h-chord</guid>
            <pubDate>2023-05-08</pubDate>
        </item>
        
        <item>
            <title>Vercel Images Optimisation</title>
            <link>https://notes.zachmanson.com/vercel-images-optimisation</link>
            
            <description>A small footgun for beginners using Vercel.</description>
            
            <content:encoded>
                <![CDATA[<p>TLDR: If you are using Vercel and display a lot of images hosted on someone else's server, you probably want to turn off <a href="https://vercel.com/docs/concepts/image-optimization">Image Optimisation</a>.</p>
<p>Over the last week I built <a href="https://alculator.zachmanson.com">Alculator</a> using Next.js and hosted on Vercel. The site uses data lovingly freebooted from the Dan Murphy's public API to rank products by the ratio of price to standard drinks, and displaying this data in a card.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/alculator-cards.png" /></p>
<p>The images for these cards are hosted on a Dan Murphy's server (media.danmurphys.com.au), and I am simply using Next.js <code>&lt;Image&gt;</code> tags to point to them.</p>
<div class="highlight"><pre><span></span><code><span class="p">...</span>
<span class="w">    </span><span class="p">&lt;</span><span class="nt">div</span><span class="w"> </span><span class="na">className</span><span class="o">=</span><span class="s">&quot;card center&quot;</span><span class="p">&gt;</span>
<span class="w">      </span><span class="p">&lt;</span><span class="nt">div</span>
<span class="w">        </span><span class="na">className</span><span class="o">=</span><span class="s">&quot;flex center-aligned&quot;</span>
<span class="w">        </span><span class="na">onClick</span><span class="o">=</span><span class="p">{()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`https://www.danmurphys.com.au/product/</span><span class="si">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">stockcode</span><span class="si">}</span><span class="sb">`</span><span class="p">)}</span>
<span class="w">      </span><span class="p">&gt;</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">Image</span>
<span class="w">          </span><span class="na">alt</span><span class="o">=</span><span class="s">&quot;Image of drink&quot;</span>
<span class="w">          </span><span class="na">height</span><span class="o">=</span><span class="s">&quot;100&quot;</span>
<span class="w">          </span><span class="na">width</span><span class="o">=</span><span class="s">&quot;80&quot;</span>
<span class="w">          </span><span class="na">src</span><span class="o">=</span><span class="p">{</span><span class="sb">`https://media.danmurphys.com.au/dmo/product/</span><span class="si">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">stockcode</span><span class="si">}</span><span class="sb">-1.png`</span><span class="p">}</span>
<span class="w">        </span><span class="p">/&gt;</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="w"> </span><span class="na">className</span><span class="o">=</span><span class="s">&quot;fill-width&quot;</span><span class="p">&gt;</span>
<span class="w">          </span><span class="p">&lt;</span><span class="nt">div</span><span class="w"> </span><span class="na">className</span><span class="o">=</span><span class="s">&quot;flex space-between align-center&quot;</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">&lt;</span><span class="nt">h3</span><span class="p">&gt;{</span><span class="nx">item</span><span class="p">.</span><span class="nx">name</span><span class="p">}&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
<span class="p">...</span>
</code></pre></div>
<p>I'm using Vercel's free tier to serve the application which grants 100GB of bandwidth, which is magnitudes higher than any amount of users I expected to serve. I put the finishing touches on the site on December 30th, sent the link to a few people and went to the pub.</p>
<p>Over the next few hours I started receiving strongly worded alerts from Vercel:</p>
<p><img alt="" src="https://notes.zachmanson.com/media/vercel-warnings.png" /></p>
<p>Either I had gone viral or my code was a lot more inefficient than I thought. At 1am I returned home, having reached the Ballmer Peak to diagnose the problem.</p>
<p>On the Vercel/Dashboard/Usage page, right at the bottom is the harmless looking graph titled "Image Optimisations" which turned out to be the culprit. By default, Vercel takes all images hosted in <code>&lt;Image&gt;</code> tags and caches them all at the Edge. Vercel had optimised 1400 of these images before I discovered this and at the free tier this is capped at 1000 images. Whoops.</p>
<p>The solution is simple:</p>
<div class="highlight"><pre><span></span><code><span class="c1">// next-config.js</span>

<span class="cm">/** @type {import(&#39;next&#39;).NextConfig} */</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">nextConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">reactStrictMode</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w">  </span><span class="nx">images</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">unoptimized</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w">  </span><span class="p">},</span>
<span class="p">};</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">nextConfig</span><span class="p">;</span>
</code></pre></div>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/vercel-images-optimisation</guid>
            <pubDate>2023-01-02</pubDate>
        </item>
        
        <item>
            <title>Imposter Syndrome Day 632</title>
            <link>https://notes.zachmanson.com/imposter-syndrome-day-632</link>
            
            <content:encoded>
                <![CDATA[<p>I have had the job title "Software Engineer" for 632 days now and it still feels like a lie most of the time.  </p>
<p>Either because</p>
<ul>
<li>it's prefixed by "graduate"</li>
<li>I tricked a floundering company in to hiring me between waves of layoffs for obscene amounts of money<ul>
<li>more money than most of the people I worked along side who were far smarter than me</li>
<li>and who got laid off with me as if we equally deserved it</li>
</ul>
</li>
<li>I tricked a company into hiring me because they liked my <a href="https://notes.zachmanson.com/minecraftle">minecraft</a> game (twice)</li>
<li><a href="https://notes.zachmanson.com/software">software</a> engineers aren't real engineers</li>
<li><a href="https://notes.zachmanson.com/web-development">web development</a> isn't even real software engineering</li>
<li>I couldn't write a compiler if you held me at gun point</li>
<li>all I do is <a href="https://notes.zachmanson.com/glue-work">glue work</a> and waste time</li>
<li>all I do is build "dashboard for x company to monitor y"</li>
<li>I don't really understand how half the tools I use work, I just know the buttons I need to press</li>
<li>when I say "backend" I mean "write db query and write some trivial transformation into JSON"</li>
<li>when I say "frontend" I mean "tailwind and knowing how <a href="https://notes.zachmanson.com/react">React</a> rendering work"</li>
<li>pipelines are just <a href="https://notes.zachmanson.com/vendorops">VendorOps</a></li>
<li>I spend all day in baby languages</li>
<li>all my technical opinions are stolen or surface level</li>
<li>I feel like I knew more in <a href="https://notes.zachmanson.com/uwa-computer-science">university</a> even though this is obviously not true<ul>
<li>I just don't have to write <a href="https://notes.zachmanson.com/c">C</a> anymore</li>
</ul>
</li>
<li>nothing I make has an interesting or novel explanation behind it</li>
<li>when I say "system administration" I mean "apt install nginx and google"</li>
<li>when I say "<a href="https://notes.zachmanson.com/open-source">open source</a>" I mean "I chuck my worthless repetitive shit under MIT"</li>
<li>for every ounce of power and efficiency the laptop I write this on has, my code still runs slow because I am so high up the stack I've never had to see the bottom</li>
<li>all I do is write tomorrow's legacy code</li>
</ul>
<p>Obviously much of this is silly and wrong and I'm doing better than most of the people I know, but all I ever do is realise how much higher the Dunning-Kruger rollercoaster ride goes.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/imposter-syndrome-day-632</guid>
            <pubDate>2024-04-03</pubDate>
        </item>
        
        <item>
            <title>Things Australia Should Steal From Other Countries</title>
            <link>https://notes.zachmanson.com/things-australia-should-steal-from-other-countries</link>
            
            <content:encoded>
                <![CDATA[<p>And vice versa.</p>
<p>Things Australia should steal:</p>
<ul>
<li>ubiquitous public transport, underground rail</li>
<li>street art</li>
<li>cheap beer</li>
<li>random very small businesses and bars</li>
<li>currywurst</li>
<li>basements</li>
<li>bike lanes everywhere</li>
<li>broader bikeshare programs</li>
<li>circle lines</li>
<li>density of population</li>
<li>ground floor is level 0</li>
<li>miniature versions of city centres in town square</li>
<li>toilet bowl shape that enables you appreciate your art (shelfstool)</li>
<li>unique streetnames</li>
<li>sex clubs, night clubs</li>
<li>people dressing up and putting effort into costumes for nights out</li>
<li>no photos in clubs</li>
<li>paying for public transport with a credit card</li>
<li>canals</li>
<li>building being old, interesting architecture</li>
<li>new venues in old spaces </li>
</ul>
<p>Things Australia does better:</p>
<ul>
<li>comfortable seats on public transport </li>
<li>cleanliness</li>
<li>water fountains</li>
<li>bathrooms</li>
<li>no toilet attendants </li>
<li>maccas</li>
</ul>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/things-australia-should-steal-from-other-countries</guid>
            <pubDate>2025-06-01</pubDate>
        </item>
        
        <item>
            <title>Slug</title>
            <link>https://notes.zachmanson.com/slug</link>
            
            <content:encoded>
                <![CDATA[<p>In <a href="https://notes.zachmanson.com/web-development">web development</a>, you sometimes come across a segment of a URL called a "slug", which is human readable string of text appended to a URL to make easier to determine the content the URL points to.  The slug itself if usually not actually necessary for web server to serve the correct page.  For example, these two links point to the same page:</p>
<p><a href="https://stackoverflow.com/questions/4230846/what-is-the-etymology-of-slug-in-a-url">https://stackoverflow.com/questions/4230846/what-is-the-etymology-of-slug-in-a-url</a></p>
<p><a href="https://stackoverflow.com/questions/4230846">https://stackoverflow.com/questions/4230846</a></p>
<p>This use of the word's use in web media is following a convention from print media, where a "slug" is an informal name given to a story before publication.  Print media takes the term from printing presses, where a slug is a line of lead cast characters.</p>
<blockquote>
<p>The term slug derives from the days of hot-metal printing, when printers set type by hand in a small form called a stick. Later huge Linotype machines turned molten lead into casts of letters, lines, sentences and paragraphs. A line of lead in both eras was known as a slug.</p>
</blockquote>
<p><cite class="standalone"><a href="https://archive.nytimes.com/www.nytimes.com/times-insider/2014/11/24/whats-in-a-slug/">Kyle Massey</a></cite></p>
<p>Screenplays have a similar concept, where a "slug line" is a heading at the start of each scene containing important general information about the following scene.</p>
<blockquote>
<p>A <em>slug line</em>, also called a <em>master scene heading</em>, occurs at the start of each scene and typically contains 3 pieces of information: whether the scene is set inside or outside (INT. or EXT.; interior or exterior), the specific location, and the time of day. Each slug line begins a new scene. In a "<a href="https://en.wikipedia.org/wiki/Shooting_script" title="Shooting script">shooting script</a>" the slug lines are numbered consecutively for ease of reference.</p>
</blockquote>
<p><cite class="standalone"><a href="https://en.wikipedia.org/wiki/Screenplay#Format_and_style">Screenplay, Wikipedia</a></cite></p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Slug_(publishing)">Slug (publishing), Wikipedia</a></li>
</ul>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/slug</guid>
            <pubDate>2023-10-21</pubDate>
        </item>
        
        <item>
            <title>Enhancement Proposal for the h Programming Language</title>
            <link>https://notes.zachmanson.com/enhancement-proposal-for-the-h-programming-language</link>
            
            <description>hEP 001 – Mathematical 𝐡  in the Standard Implementation</description>
            
            <content:encoded>
                <![CDATA[<table>
    <tr><td>hEP</td><td>001</td></tr>
    <tr><td>Title</td><td>Mathematical 𝐡 in the Standard Implementation</td></tr>
    <tr><td>Author</td><td>Zach Manson</td></tr>
    <tr><td>Status</td><td>Proposed</td></tr>
    <tr><td>Type</td><td>Feature</td></tr>
    <tr><td>hlang-version</td><td>1.0</td></tr>
</table>

<h2 id="abstract">Abstract</h2>
<p>This hEP proposes a framework for adding first-class support for additional forms of h used in mathematical contexts, and that "𝐡" be added to the h programming language standard implementation. As of version 1.0, the only supported output characters are "h" and "'".</p>
<h2 id="motivation">Motivation</h2>
<p>The h Programming language is borne from <a href="https://christine.website/blog/formal-grammar-of-h-2019-05-19">the conlang h</a> which was formalised in 2019 by Xe Iaso.</p>
<p>In their unveiling of the language they exhibited how the conlang's simple syntax could be used for complex conversation, all within the use of a single written character and phone:</p>
<div class="highlight"><pre><span></span><code>&lt;Cadey&gt; h
&lt;DoesntGetIt&gt; h h h h
* Cadey facepalms
</code></pre></div>
<p>As stated in h's formal specification, it can be used to represent all possible communications within the confines of the letter h.</p>
<p>They later developed a syntax, compiler and runtime for <a href="https://christine.website/blog/h-language-2019-06-30">the h Programming Language</a> (hlang). This programming language was designed for the output of the h conlang, and expanded the character set in use to include both the Romanic form "h" and its Lojbanic equivalent "'".</p>
<p>A long-standing difficulty for the programming language is expressing mathematical notation in its output. While it is entirely possible to express any and all equations in the language, for this purpose it doesn't utiltise the "h" character to its maximum capacity and as such is unnecessarily verbose.</p>
<h2 id="goals">Goals</h2>
<p>The primary goals of this proposal are to provide a specification for implementing additional forms of "h" in hlang to enable more terse output for use in mathematical contexts. The only additional form of "h" proposed in this hEP is "𝐡" (U+1D421).</p>
<p>The proposal would enable programs such as this:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>cat<span class="w"> </span>new_equation.h
𝐡<span class="w"> </span>𝐡<span class="w"> </span>h<span class="w"> </span><span class="err">&#39;</span>
</code></pre></div>
<p>AST this would generate during compilation:</p>
<div class="highlight"><pre><span></span><code>H(&quot;𝐡 𝐡 h &#39;&quot;)
</code></pre></div>
<p>Program output:</p>
<div class="highlight"><pre><span></span><code>𝐡 𝐡 h &#39;
</code></pre></div>
<p>This is an improvement over the existing implementation, which would require the much longer and more cumbersome hlang 1.0 source to produce the equivalent equation:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>cat<span class="w"> </span>old_equation.h
h<span class="w"> </span>h<span class="w"> </span>h<span class="w"> </span><span class="s1">&#39; &#39;</span><span class="w"> </span>h<span class="w"> </span><span class="s1">&#39; h h h h h h h h h &#39;</span><span class="w"> </span>h
</code></pre></div>
<p>Translating this from h (conlang) to English:</p>
<div class="highlight"><pre><span></span><code>a^2 + b^2 = c^2
</code></pre></div>
<h2 id="non-goals">Non-Goals</h2>
<p>This hEP is not an attempt to sully hlang with characters outside of the formal grammar of the h conlang. The scope of the specification, while allowing for additional forms of "h" to be supported, is exclusively for forms of "h". It is not applicable to other characters, such as "g".</p>
<p>This hEP only explicitly proposes the addition of "𝐡" (U+1D421) though is designed to allow other forms of "h" to be supported by hlang in the future, such as "𝒉" (U+1D489), "𝓱" (U+1D4F1), or "𝕙" (U+1D559). While these forms could be implemented in a near identical manner to "𝐡", that is outside of the scope of this hEP. All of these forms of "h" are UTF-8 characters designed for use in mathematical equations.</p>
<p>The hEP is also explicitly not intending to enable support for "h" with diacritics, such as "ḧ" (U+1E27). Etymologically related forms of modern "h" are also outside the scope of this hEP.</p>
<h2 id="semantics">Semantics</h2>
<p>Under this proposal, "𝐡" would receive first class support by hlang. To output it would be the same as the standard "h" character:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>cat<span class="w"> </span>math_h.h
𝐡
</code></pre></div>
<p>Produced AST:</p>
<div class="highlight"><pre><span></span><code>H(𝐡)
</code></pre></div>
<p>Output:</p>
<div class="highlight"><pre><span></span><code>𝐡
</code></pre></div>
<p>Represented with a node tree:</p>
<div class="highlight"><pre><span></span><code>&amp;peg.Node{
    Name: &quot;H&quot;,
    Text: &quot;𝐡&quot;,
    Kids: nil,
}
</code></pre></div>
<p>The node tree for the earlier Pythagorean example would be:</p>
<div class="highlight"><pre><span></span><code>&amp;peg.Node{
    Name: &quot;H&quot;,
    Text: &quot;𝐡 𝐡 h &#39;&quot;,
    Kids: {
        &amp;peg.Node{
            Name: &quot;&quot;,
            Text: &quot;𝐡&quot;,
            Kids: nil,
        },
        &amp;peg.Node{
            Name: &quot;&quot;,
            Text: &quot;𝐡&quot;,
            Kids: nil,
        },
        &amp;peg.Node{
            Name: &quot;&quot;,
            Text: &quot;h&quot;,
            Kids: nil,
        },
        &amp;peg.Node{
            Name: &quot;&quot;,
            Text: &quot;&#39;&quot;,
            Kids: nil,
        },
    },
}
</code></pre></div>
<p>The "𝐡" character should be usable in all contexts where "h" and "'" are currently usable. Grammatically it should follow all the same principles and restrictions as "h" and "'".</p>
<h2 id="dependencies">Dependencies</h2>
<p>As with hlang v1.0, this proposal not require any dependencies, other than UTF-8 formatted source files.</p>
<h2 id="acknowledgements">Acknowledgements</h2>
<p>Thanks to Xe Iaso for creating h and hlang, as well as all their other incredible projects.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/enhancement-proposal-for-the-h-programming-language</guid>
            <pubDate>2022-03-18</pubDate>
        </item>
        
        <item>
            <title>Spotify Design Decisions</title>
            <link>https://notes.zachmanson.com/spotify-design-decisions</link>
            
            <content:encoded>
                <![CDATA[<p>This page is a list of <a href="https://notes.zachmanson.com/design">Design</a> decisions made by Spotify that I consider shitty.  I'm sure some of them were only A/B tested, coming and going silently, but some have lingered for years. In any case I have been subjected to all of these personally.</p>
<p>See also: <a href="https://www.todepond.com/wikiblogarden/work/dear-spotify/">Dear spotify product manager</a> by Lu Wilson, <a href="https://www.youtube.com/watch?v=suhEIUapSJQ">Redesigning Spotify</a> by Juxtopposed</p>
<h2 id="unable-to-remove-dj-shortcut-on-desktop">Unable to Remove DJ Shortcut On Desktop</h2>
<p>On desktop they have added a link to the DJ feature at the top of Your Library, below pinned playlists.  Despite not being a playlist (nor a feature I use), it continues to appear even when you have set the filter to only show playlists.  As far as I know it cannot be removed?  But it can be pinned.  Despite already being permalocked to the top.
<img alt="" src="https://notes.zachmanson.com/media/dj.png" />
The day after writing the previous paragraph, I noticed they added the DJ to the same section in the mobile app. In the mobile app a long press on the DJ icon does shows you an option to hide it, which seems to sync across all my devices.  Why is this option not available in the desktop client!</p>
<h2 id="custom-playlist-sorting">Custom Playlist Sorting</h2>
<p>For as long as I can remember you can order the playlists you have created/followed in any order along the left side of the desktop app. You can even put them in nested folders which is fantastic.  I have many playlists organised into folders in particular orders.  I know how to get to the playlist I want quickly because I have put it in a particular place. This custom order and "Recently played" are the only two i would ever want.</p>
<p>Spotify seems to hate this use case.  To my knowledge its never been possible to rearrange order of playlists on the mobile app, only the desktop client.</p>
<p>When you attempt to add a song to playlist on mobile, there isn't even an option to sort by recently played anymore.  This is diametrically opposed to desktop, where the right click menu will ONLY allow you to sort by custom order. </p>
<p><img alt="" src="https://notes.zachmanson.com/media/no-custom-order.png" /></p>
<h2 id="smart-shuffle-button-cycle-lock">Smart Shuffle Button Cycle Lock</h2>
<p>Spotify has added a feature called Smart Shuffle that works like normal shuffling, but also adds recommended songs in-between ever few tracks in the queue.  On paper I like this feature, though I have never found myself actually using it. I was introduced to it through the following infuriating UX.</p>
<p>I go the the Now Playing screen on the Android Spotify client. The playlist I am listening to is currently set to shuffle (regular shuffle), but I would like to turn off regular shuffle.  I press the button with a shuffle icon, which has for 10+ years been a single toggle that would instantly take effect.  Instead of a simple 2 option toggle, I find that it has been converted in to a 3 option cycle button, where pressing it cycles through the options of <code>Linear -&gt; Shuffle -&gt; Smart Shuffle -&gt; Linear</code>. I can see the thinking behind this decision, but it breaks muscle memory established all other music players for decades.  That is not my issue though.</p>
<p>When you click the button and move the state from <code>Shuffle</code> to <code>Smart Shuffle</code>, the application begins loading the Smart Shuffle recommendations which means it must query Spotify servers, which is an operation that takes multiple seconds.  While this occurs, the button with a Shuffle icon becomes a button with a loading spinner, and becomes unclickable. I must wait multiple seconds before I can turn off Smart Shuffle.</p>
<p><code>Linear -&gt; Shuffle -&gt; Loading (multiple seconds) -&gt; Smart Shuffle -&gt; Linear</code></p>
<p>This is deeply infuriating.  I never even wanted to use Smart Shuffle in the first place and now I must wait for it.</p>
<p>This occurred every time I wanted to turn off  <code>Shuffle</code>.  Which is a lot.</p>
<p>As of 2024-01-21, this loading state of the button appears to have been removed, thankfully.</p>
<h2 id="smart-shuffle-cycle-order">Smart Shuffle Cycle Order</h2>
<p>Oh sorry did I say the order was <code>Linear -&gt; Shuffle -&gt; Smart Shuffle</code>?  Its actually <code>Linear -&gt; Smart Shuffle -&gt; Shuffle</code>.  Wait no go back.  Wait... it's a different order on my phone vs on desktop? This is real wtf.</p>
<h2 id="smart-shuffle-popup">Smart Shuffle Popup</h2>
<p>I clicked shuffle once and this came up. Thankfully it only ever appeared once on 2023-11-14, but it never should have in the first place. </p>
<p><img alt="" src="https://notes.zachmanson.com/media/smart-shuffle-popup.png" /></p>
<h2 id="overflow-menu-loading">Overflow Menu Loading</h2>
<p>For some godforsaken reason, the overflow menu for a track sometimes appears to require a network request before it can show.  If this request is slow, it will show a loading spinner in place of the menu, until the server responds.  Or in some cases, the <strong>overflow menu will fail to load entirely</strong>.  This is insane, since for the most part, the options on this menu are identical.  This is doubly insane since the menu can load without any issue what-so-ever in offline mode.</p>
<p>I have only ever experienced this issue on the Android mobile app, which for many years did not support swiping on a song to queue a song.  At this time you were only able to queue a song. through the overflow menu, which often times meant waiting multiple seconds for menu to appear.  God forbid you wanting to queue multiple songs in a row. Thankfully you can now bypass the menu loading by swiping a track to the right.</p>
<p>Oh wait!</p>
<h2 id="cant-swipe-to-queue-on-blend-playlists">Can't Swipe to Queue on Blend Playlists</h2>
<p>On Android you cannot swipe to queue a playlist if the playlist is a blend playlist??? Why would this screen use a different component to represent a list of songs to every other screen in the app? </p>
<video src='/media/blend-swiping.webm' controls></video>

<h2 id="secret-sliding-menu">Secret Sliding Menu</h2>
<p>This one I only just noticed now while typing this post. There is a sliding menu that appears if you click on your profile picture in the corner of any of the 3 main screens.  It seems a bit sparse, like it would be better suited to being a drop down? In any case this menu can actually be accessed from any playlist folder screen as well, despite there being no indication of this.  This only seems to work on playlist folder screens though, as it doesn't work for any other subpages.</p>
<p>This one was annoying as I accidentally triggered in when attempting to swipe in from the left to go back a screen.</p>
<video src='/media/sliding-menu.webm' controls></video>

<h2 id="cant-swipe-up-to-access-player">Can't Swipe Up to Access Player</h2>
<p>On Android you can swipe down on the player interface but you cannot swipe up to reveal it.  When you attempt this you will almost definitely skip the song that is currently playing, since the minimised version of the player only detects left and right swipes.</p>
<video src='/media/swipe-up.webm' controls></video>

<h2 id="top-bar-alignment">Top Bar Alignment</h2>
<p>Who the fuck let this happen.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/spotify-top-bar.png" /></p>
<h2 id="liked-songs-filters-are-actually-a-playlist">Liked Songs Filters Are Actually a Playlist</h2>
<p>For years, if you used the text filter on your Liked Songs and then played one of the songs, the queue would only populate with songs that matched that filter.  This could be useful for only listening to a particular artist within your Liked Songs, but was usually just annoying.</p>
<p>As of 2024 it appears this behaviour has been removed.</p>
<h2 id="search-results-are-actually-a-playlist">Search Results Are Actually a Playlist</h2>
<p>Similar to the previous issue, if you searched for a song using the search tab, and then played one of the results, the queue would populate with all the other search results meaning you would hear the same song repeated several times, or covers, or random other tracks with similar names.</p>
<p>As of 2024 it appears this behaviour has been removed.</p>
<h2 id="useless-music-video-indicator">Useless Music Video Indicator</h2>
<p>They have added a subtitle and icon for songs with music videos, which looks like it is clickable or has some feature attached to it. But it just... doesn't?</p>
<p><img alt="" src="https://notes.zachmanson.com/media/spotify-music-video.png" /></p>
<h2 id="upcoming-complaints">Upcoming Complaints</h2>
<ul>
<li>Rapidly toggling through Shuffle button states can cause the cycle to desync in strange ways</li>
<li>Spotify (regular) Shuffle algorithm has some problems</li>
</ul>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/spotify-design-decisions</guid>
            <pubDate>2024-03-17</pubDate>
        </item>
        
        <item>
            <title>Federating with Meta</title>
            <link>https://notes.zachmanson.com/federating-with-meta</link>
            
            <content:encoded>
                <![CDATA[<p>Meta's rumoured Twitter competitor Threads has caused a stir in the <a href="https://notes.zachmanson.com/fediverse">Fediverse</a>. Some suggesting fedi instances should preemptively block Threads, others willing to wait and see what happens.  At the time of writing (June 2023) it is unclear how this will pan out. Many fear the threat of <a href="https://en.wikipedia.org/wiki/Embrace,_extend,_and_extinguish">embrace, extend, extinguish</a>, but others are excited at the prospect of the fediverse becoming more significant.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/meta-fediverse.jpeg" /></p>
<p>Meta: "MAY I JOIN YOU?"<br />
<a href="https://framapiaf.org/@davidrevoy/110583258129951932">By David Revoy</a></p>
<p>One of the oft-touted benefits of the fediverse is that it would be resilient to corporate <a href="https://notes.zachmanson.com/tiktoks-enshittification">enshittification</a>. It will be interesting to see how the fediverse handles real threats.  </p>
<h2 id="interesting-articles">Interesting Articles</h2>
<p>Roelant Kooij wrote an <a href="https://roelant.net/en/2023/interesting-reads-on-meta-vs-fediverse/">excellent overview</a> of the arguments on either side of this debate.  The Fediverse Report also has a <a href="https://fediversereport.com/last-week-in-the-fediverse-episode-24/">good timeline</a> of the news.  These are sorted in the order I have found them, with my <strong>strongest recommendations in bold</strong>.</p>
<ul>
<li><a href="https://fedipact.online/">Not That Kind of ‘Open’</a>, John Gruber on the debate <ul>
<li>suggests that the pre-emptive block may be "petty and deliberately insular"</li>
</ul>
</li>
<li><a href="https://daringfireball.net/2023/06/more_on_preemptively_blocking">More on Preemptively Blocking Facebook’s Imminent ActivityPub Entry</a>, John Gruber's follow-up<ul>
<li>suggests that if Facebook is federated, they should start with "two strikes against it"</li>
<li>"Is the goal of the Fediverse to be anti-corporate/anti-commercial, or to be pro-openness? I think openness is the answer"</li>
</ul>
</li>
<li><a href="https://ploum.net/2023-06-23-how-to-kill-decentralised-networks.html">How to Kill a Decentralised Network (such as the Fediverse)</a>, Ploum's excellent writeup on how large companies can (sometimes maliciously) kill open/decentralised networks<ul>
<li>makes comparison to Google killing XMPP and Microsoft using OOXML to cripple open source office software</li>
</ul>
</li>
<li>⭐️ <a href="https://about.scicomm.xyz/doku.php?id=blog:2023:0625_meta_on_the_fediverse_to_block_or_not_to_block"><strong>Meta on the Fediverse: to block or not to block?</strong></a>, SciComm admins writeup of their balanced thought process on pre-emptive blocking<ul>
<li>they conclude to pre-emptively block, but leave the door open to unblocking if Meta is a decent fediverse citizen</li>
</ul>
</li>
<li><a href="https://reb00ted.org/tech/20230625-meta-why-activitypub/">Why would Meta implement ActivityPub? 1½ reasons are compelling, another is not</a>, Johannes Ernst's analysis of Meta's motivations<ul>
<li>suggests EEE is not worthwhile for Meta, far less important than appeasing incoming EU regulations</li>
</ul>
</li>
<li>Tim Chamber's (indieweb.social admin) <em>First Time?</em> series of posts on the matter<ul>
<li><a href="https://www.timothychambers.net/2023/06/23/project-and-the.html">Project92 and the Fediverse - A Smarter Battle Plan to Protect the Open Social Web</a><ul>
<li>suggests that pre-emptive defederation is not the most effective strategy to protect the current fediverse</li>
<li>thinks current fediverse can win by providing a better experience, especially in areas where corporate interests conflict with this (e.g. advertising)</li>
</ul>
</li>
<li><a href="https://www.timothychambers.net/2023/06/25/project-the-fediverse.html">Project92, the Fediverse: (First Time? Part 2)</a><ul>
<li>'We have the tools to protect our people, and we are stronger to "Watch like a hawk, with our fingers over the block button."'</li>
</ul>
</li>
<li><a href="https://www.timothychambers.net/2023/06/30/meta-and-the.html">Meta and the Fediverse (First Time? Part 3)</a><ul>
<li>generally calling for calming of vitriol, promoting compassion between disagreeing groups</li>
<li>"All sides should admit we don’t know far more than we do know right now."</li>
</ul>
</li>
<li><a href="https://www.timothychambers.net/2023/07/03/instagram-threads-and.html">Instagram Threads and the Fediverse (First Time? Part 4)</a>, a summation of all his previous posts</li>
</ul>
</li>
<li>⭐️ <a href="https://heat-shield.space/mastodon_two_camps.html"><strong>The Two Camps of Mastodon</strong></a>, on the general split between people that view the fediverse as a collection of small communities and those that view it as a distributed network where most people can talk to most others in a decentralised fashion, and how Threads' existence exacerbate this divide</li>
<li><a href="https://netwars.pelicancrossing.net/2023/06/30/the-horns-of-a-dilemma/">The horns of a dilemma</a>, Wendy M. Grossman enumerating potential futures</li>
<li>⭐️ <a href="https://blog.bloonface.com/2023/07/03/meta-and-the-fediverse-sorting-heat-from-light/"><strong>Meta and the fediverse: sorting heat from light</strong></a>, a fantastic rebuke to many of Meta's alleged nefarious motivations<ul>
<li>meta gains little in terms of data collection ability since <a href="https://notes.zachmanson.com/activitypub">ActivityPub</a> lacks privacy anyway, and what it does gain isn't very useful, and the current fediverse population size is not large enough to be worth pursuing for data collection</li>
<li>its seems unlikely that Meta actually sees the fediverse as a threat</li>
<li>the Gchat/XMPP embrace/extend/extinguish example is flawed</li>
<li>Meta isn't evil without motivation</li>
<li>"So why <em>is</em> Meta going to use ActivityPub?  Here’s the thing; <em>I haven’t the faintest fucking clue</em>.  It does not obviously help Meta in any way to have interoperability with ActivityPub"</li>
</ul>
</li>
<li><a href="https://tracks.ranea.org/post/722507935765397504/youre-so-vain-you-probably-think-this-app-is">You’re So Vain, You Probably Think This App Is About You</a>, an argument against the idea that Meta actually wants to EEE the fediverse<ul>
<li>suggests that there is little monetary reason to federate from Meta's perspective, other than regulatory pressure</li>
<li>particularly posits that defederating from servers that refuse to defederate Meta is particularly short-sighted</li>
</ul>
</li>
</ul>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/federating-with-meta</guid>
            <pubDate>2023-06-01</pubDate>
        </item>
        
        <item>
            <title>Fixing Lightspeed Ramming</title>
            <link>https://notes.zachmanson.com/fixing-lightspeed-ramming</link>
            
            <content:encoded>
                <![CDATA[<p>The lightspeed ram in The Last Jedi is incredible but it does break the world. </p>
<p>It is such a devastating attack that it's crazy nobody in-universe has tried it before, especially given the existence of autonomous robots that can operate ships. The film provides no counter to this attack. It's not unreasonable to wonder what kind of impact this style of attack could have had on the Death Star.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/HoldoManeuver.png" /></p>
<p>The Last Jedi film doesn't provide any explanation for why this has never been tried before. The novelisation has an explanation, stating the the Raddus has experimental shields allowing it to do this, but this is not in the text of the film at all. Further, it is contradicted in The Rise of Skywalker's
attempted explanation which states that the "Holdo Maneuver" was "one in a million". That explanation feels unsatisfactory to me, because it mean that Holdo either:</p>
<ol>
<li>was extremely lucky and noticed that the conditions were just right for it to work so went for it</li>
<li>was risking everything when the attack was extremely unlikely to work</li>
</ol>
<p>Neither of those explanations seem to line up with the way it is portrayed in The Last Jedi. The idea that the Holdo Maneuver is extremely rare doesn't seem to make sense even within the context of The Rise of Skywalker, given that:</p>
<ol>
<li>another Holdo Maneuver is shown at the end of the movie above the moon of Endor</li>
<li>the battle on Exegol seems like the <strong>perfect</strong> set up for a Holdo Maneuver, with thousands of Final Order capital ships, extremely close together</li>
</ol>
<p>The Last Jedi itself is excruciatingly close to a sensible explanation that works in-universe: <strong>you can only lightspeed ram a ship that is attempting to track you through hyperspace</strong>.</p>
<p>The more I think about this the more sense it makes. </p>
<p>This places an interesting limit both mechanics introduced in The Last Jedi. By tracking a ship through hyperspace, you are opening yourself up to a potentially devastating attack, but an attack that likely will cost the enemy heavily too. If your enemy suspects that you're going to ram them, they can turn off their hyperspace tracking equipment, which protects them but may allow you an opportunity to escape.</p>
<p>It also creates a logical reason why a droid-piloted lightspeed ram wouldn't have worked in The Last Jedi, since it is well established that ships can be scanned for the presence of lifeforms. Your enemy could detect that your ship has no lifeforms aboard, surmise you are planning on ramming them, giving them the opportunity to counter.</p>
<p>The pseudoscience of how this works could be easily explained, even within limited explanation that hyperspace has in Star Wars. </p>
<blockquote>
<p>A Resistance engineer (perhaps named Rose) hurriedly pushes past officers on the bridge, panting as she reaches Admiral Holdo. A sergeant begins to berate her for forcing her way onto the bridge but he is put at ease by the Admiral. Holdo asks the engineer what's so important it warrants the commotion.</p>
<p>"If they're tracking us through hyperspace, their ship must be in normal space and hyperspace at the same time" says the engineer as she catches her breath.</p>
<p>Holdo's expression drops. She pauses, eyes calculating. What the engineer is intimating has never been done before. The room is silent.</p>
<p>Holdo snaps back to reality, ordering the bridge be cleared and the ship evacuated.</p>
</blockquote>
<p>This cleanly explains why this has never been done before, it wasn't possible until hyperspace tracking was developed. This battle will have implications for all future battles, where any ship capable of hyperspace tracking introduces the possibility mutually assured destruction</p>
<p>Alas, what could have been.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/fixing-lightspeed-ramming</guid>
            <pubDate>2025-10-01</pubDate>
        </item>
        
        <item>
            <title>The Poor Man's Static Site Generator</title>
            <link>https://notes.zachmanson.com/the-poor-mans-static-site-generator</link>
            
            <description>Using Pandoc to well below its fullest potential!</description>
            
            <content:encoded>
                <![CDATA[<p><a href="https://pandoc.org/">Pandoc</a> is a Swiss Army knife that I exclusively use to cut cheese.</p>
<p>Every time I begin to write for this site, I open Codium and create a new file in my repo. Then I realise I meant to create a new folder, call myself an idiot and start over. This is the first step of my process, which currently concludes with running my anemic static site generator. I hope to soon replace with this a bespoke and iron proficient(?) site generator, but until then I will continue using Pandoc to fulfill my static site generator requirements.</p>
<p>My static site generator requirements:</p>
<ul>
<li>Markdown input</li>
<li>Code highlighting</li>
<li>with theming?</li>
<li>Simple templating</li>
<li>Minimum (ideally 0) dependencies</li>
<li>Automated?</li>
</ul>
<h2 id="markdown-input">Markdown Input</h2>
<p>It can convert documents between myriad formats, most relevantly Markdown to HTML.</p>
<div class="highlight"><pre><span></span><code>pandoc<span class="w"> </span>inputfile.md<span class="w"> </span>-o<span class="w"> </span>output.html<span class="w"> </span>--to<span class="o">=</span>html5
</code></pre></div>
<h2 id="code-highlighting">Code Highlighting</h2>
<p>There are plenty of tools that can do this, but Pandoc was the first I found that had code highlighting without having any depedencies like <a href="https://highlightjs.org/">highlight.js</a>, just saving the colour information in inline CSS</p>
<p>Any fenced Markdown code blocks with a language specified (like the following) will have the syntax automatically highlighted by Pandoc.</p>
<div class="highlight"><pre><span></span><code><span class="gh"># Title</span>

<span class="gu">## Subtitle</span>

Lorem ipsum amirite.  Hey look a fenced code block:

<span class="sb">```Python</span>
<span class="k">if</span> <span class="n">x</span> <span class="o">==</span> <span class="mi">5</span><span class="p">:</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;x is equal to 5&quot;</span><span class="p">)</span>
<span class="sb">```</span>
</code></pre></div>
<p>Pandoc will turn this Markdown into the following <a href="https://notes.zachmanson.com/html">HTML</a>:</p>
<div class="highlight"><pre><span></span><code><span class="p">&lt;</span><span class="nt">h1</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;title&quot;</span><span class="p">&gt;</span>Title<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">h2</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;subtitle&quot;</span><span class="p">&gt;</span>Subtitle<span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Lorem ipsum amirite. Hey look a fenced code block:<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;sourceCode&quot;</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;cb3&quot;</span><span class="p">&gt;&lt;</span><span class="nt">pre</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;sourceCode python&quot;</span><span class="p">&gt;&lt;</span><span class="nt">code</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;sourceCode python&quot;</span><span class="p">&gt;&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;cb3-1&quot;</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&quot;#cb3-1&quot;</span> <span class="na">aria-hidden</span><span class="o">=</span><span class="s">&quot;true&quot;</span> <span class="na">tabindex</span><span class="o">=</span><span class="s">&quot;-1&quot;</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;cf&quot;</span><span class="p">&gt;</span>if<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span> x <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;op&quot;</span><span class="p">&gt;</span>==<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;dv&quot;</span><span class="p">&gt;</span>5<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>:<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;cb3-2&quot;</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&quot;#cb3-2&quot;</span> <span class="na">aria-hidden</span><span class="o">=</span><span class="s">&quot;true&quot;</span> <span class="na">tabindex</span><span class="o">=</span><span class="s">&quot;-1&quot;</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>    <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;bu&quot;</span><span class="p">&gt;</span>print<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>(<span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;st&quot;</span><span class="p">&gt;</span>&quot;x is equal to 5&quot;<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>)<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">code</span><span class="p">&gt;&lt;/</span><span class="nt">pre</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div>
<p>The various HTML tag class spaghetti are used to highlight the code, though the CSS required to actually make this work is not included in the standard Pandoc command invocation. To actually generate the CSS that makes these classes work you need to use the <code>--standalone</code> flag, à la:</p>
<div class="highlight"><pre><span></span><code>pandoc<span class="w"> </span>inputfile.md<span class="w"> </span>-o<span class="w"> </span>output.html<span class="w"> </span>--to<span class="o">=</span>html5<span class="w"> </span>--standalone
</code></pre></div>
<p>This will include some inline CSS, along the lines of:</p>
<div class="highlight"><pre><span></span><code><span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">al</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#ff0000</span><span class="p">;</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Alert */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">an</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#60a0b0</span><span class="p">;</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span><span class="w"> </span><span class="k">font-style</span><span class="p">:</span><span class="w"> </span><span class="kc">italic</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Annotation */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">at</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#7d9029</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Attribute */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">bn</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#40a070</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* BaseN */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">bu</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* BuiltIn */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">cf</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#007020</span><span class="p">;</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* ControlFlow */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">ch</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#4070a0</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Char */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">cn</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#880000</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Constant */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">co</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#60a0b0</span><span class="p">;</span><span class="w"> </span><span class="k">font-style</span><span class="p">:</span><span class="w"> </span><span class="kc">italic</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Comment */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">cv</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#60a0b0</span><span class="p">;</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span><span class="w"> </span><span class="k">font-style</span><span class="p">:</span><span class="w"> </span><span class="kc">italic</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* CommentVar */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">do</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#ba2121</span><span class="p">;</span><span class="w"> </span><span class="k">font-style</span><span class="p">:</span><span class="w"> </span><span class="kc">italic</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Documentation */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">dt</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#902000</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* DataType */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">dv</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#40a070</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* DecVal */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">er</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#ff0000</span><span class="p">;</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Error */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">ex</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Extension */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">fl</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#40a070</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Float */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">fu</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#06287e</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Function */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">im</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Import */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">in</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#60a0b0</span><span class="p">;</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span><span class="w"> </span><span class="k">font-style</span><span class="p">:</span><span class="w"> </span><span class="kc">italic</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Information */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">kw</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#007020</span><span class="p">;</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Keyword */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">op</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#666666</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Operator */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">ot</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#007020</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Other */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">pp</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#bc7a00</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Preprocessor */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">sc</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#4070a0</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* SpecialChar */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">ss</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#bb6688</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* SpecialString */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">st</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#4070a0</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* String */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">va</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#19177c</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Variable */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">vs</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#4070a0</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* VerbatimString */</span>
<span class="nt">code</span><span class="w"> </span><span class="nt">span</span><span class="p">.</span><span class="nc">wa</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#60a0b0</span><span class="p">;</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span><span class="w"> </span><span class="k">font-style</span><span class="p">:</span><span class="w"> </span><span class="kc">italic</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Warning */</span>
</code></pre></div>
<p>This CSS is great, but only actually needs to be generated once. I copied this CSS in a single <a href="https://github.com/pavo-etc/pavo-etc.github.io/blob/94d59de43188660d625d12b5df33894841f99922/styles/code.css">file</a>. After copying this text, the <code>--standalone</code> flag can be omitted.</p>
<h3 id="with-theming">with theming?</h3>
<p>The colours used in the highlighting can be customised! As with everything else in my life at the moment, I use <a href="https://draculatheme.com/">Dracula</a>, which thankfully <a href="https://draculatheme.com/pandoc">already has a theme for Pandoc</a>.</p>
<p>Once the theme is installed, Pandoc can highlight using the theme with the <code>--defaults</code> flag:</p>
<div class="highlight"><pre><span></span><code>pandoc<span class="w"> </span>inputfile.md<span class="w"> </span>-o<span class="w"> </span>output.html<span class="w"> </span>--to<span class="o">=</span>html5<span class="w"> </span>--standalone<span class="w"> </span>--defaults<span class="w"> </span>path/to/theme/dracula.yaml
</code></pre></div>
<p>The HTML classes will be the same names regardless of the theme, but the inline CSS will be dependent on the theme. Again, this only needs to be run once, and the inline CSS can be copied and saved to be used later.</p>
<h2 id="simple-templating">Simple Templating</h2>
<p>Pandoc makes it simple, as long as you only need simple template insertions! HTML templates can be created easily. A basic template could look like the following:</p>
<div class="highlight"><pre><span></span><code><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&quot;stylesheet&quot;</span> <span class="na">href</span><span class="o">=</span><span class="s">&quot;/styles/code.css&quot;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>$title$<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>

<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">header</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>$title$<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>$subtitle$<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>$date$<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">header</span><span class="p">&gt;</span>

    <span class="p">&lt;</span><span class="nt">article</span><span class="p">&gt;</span>
        $body$
    <span class="p">&lt;/</span><span class="nt">article</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
</code></pre></div>
<p>Note the inclusion of the stylesheet. <code>code.css</code> includes the CSS for code highlighting.</p>
<p>The <code>body</code> variable is automatically assigned to the content of the Markdown file, and the rest of the variables can be assigned manually using a YAML header in the Markdown file.</p>
<div class="highlight"><pre><span></span><code>---
title: A Titular Title
subtitle: A subtitular subtitle
<span class="gu">date: 2022-06-12</span>
<span class="gu">---</span>

<span class="gu">## Chapter 1: Where it all began</span>

Lorem ipsum amirite.  Hey look a fenced code block:

<span class="sb">```Python</span>
<span class="k">if</span> <span class="n">x</span> <span class="o">==</span> <span class="mi">5</span><span class="p">:</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;x is equal to 5&quot;</span><span class="p">)</span>
<span class="sb">```</span>
</code></pre></div>
<p>To use the template, you use the <code>--template</code> flag in the Pandoc invocation:</p>
<div class="highlight"><pre><span></span><code>pandoc<span class="w"> </span>inputfile.md<span class="w"> </span>-o<span class="w"> </span>outputfile.html<span class="w"> </span>--to<span class="o">=</span>html5<span class="w"> </span>--template<span class="o">=</span>path/to/template.html
</code></pre></div>
<h2 id="dependencies">Dependencies</h2>
<p>None! The outputted HTML has no external dependencies.</p>
<h2 id="automated">Automated?</h2>
<p>It's a pain typing out that command for each post - so don't.</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/usr/bin/env bash</span>

<span class="nv">nonposts</span><span class="o">=(</span><span class="s2">&quot;styles/&quot;</span>,<span class="w"> </span><span class="s2">&quot;templates/&quot;</span><span class="o">)</span>

<span class="k">for</span><span class="w"> </span>d<span class="w"> </span><span class="k">in</span><span class="w"> </span>*/<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">    </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$d</span><span class="s2">&quot;</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span>!<span class="w"> </span><span class="o">[[</span><span class="w"> </span><span class="si">${</span><span class="nv">nonposts</span><span class="p">[*]</span><span class="si">}</span><span class="w"> </span><span class="o">=</span>~<span class="w"> </span><span class="nv">$d</span><span class="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">        </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;is post&quot;</span>
<span class="w">        </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;generating index.html&quot;</span>
<span class="w">        </span><span class="nv">name</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$d</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span><span class="s1">&#39;s/.$//&#39;</span><span class="k">)</span>
<span class="w">        </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;pandoc </span><span class="si">${</span><span class="nv">d</span><span class="si">}${</span><span class="nv">name</span><span class="si">}</span><span class="s2">.md -o </span><span class="si">${</span><span class="nv">d</span><span class="si">}</span><span class="s2">index.html --to=html5 --template=templates/template.html&quot;</span>
<span class="w">        </span>pandoc<span class="w"> </span><span class="si">${</span><span class="nv">d</span><span class="si">}${</span><span class="nv">name</span><span class="si">}</span>.md<span class="w"> </span>-o<span class="w"> </span><span class="si">${</span><span class="nv">d</span><span class="si">}</span>index.html<span class="w"> </span>--to<span class="o">=</span>html5<span class="w"> </span>--template<span class="o">=</span>templates/template.html
<span class="w">    </span><span class="k">fi</span>
<span class="k">done</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;done&quot;</span>
</code></pre></div>
<p>This script is designed to work on the following directory structure:</p>
<div class="highlight"><pre><span></span><code>blog
├──<span class="w"> </span>basic-seleniumbasic
│<span class="w">   </span>├──<span class="w"> </span>adding-reference.png
│<span class="w">   </span>├──<span class="w"> </span>basic-seleniumbasic.md
│<span class="w">   </span>├──<span class="w"> </span>dotnet-frameworks.png
│<span class="w">   </span>├──<span class="w"> </span>index.html
│<span class="w">   </span>└──<span class="w"> </span>seleniumbasic-installer.png
├──<span class="w"> </span>covid-updates
│<span class="w">   </span>├──<span class="w"> </span>covid-updates.md
│<span class="w">   </span>├──<span class="w"> </span>index.html
│<span class="w">   </span>└──<span class="w"> </span>media_release.png
├──<span class="w"> </span>crafting-recipes
│<span class="w">   </span>├──<span class="w"> </span>crafting-recipes.md
│<span class="w">   </span>└──<span class="w"> </span>index.html
├──<span class="w"> </span>file-descriptors
│<span class="w">   </span>├──<span class="w"> </span>file-descriptors.md
│<span class="w">   </span>└──<span class="w"> </span>index.html
├──<span class="w"> </span>generate_posts.sh
├──<span class="w"> </span>hep-001
│<span class="w">   </span>├──<span class="w"> </span>hep-001.md
│<span class="w">   </span>└──<span class="w"> </span>index.html
├──<span class="w"> </span>index.html
├──<span class="w"> </span>poor-mans-site
│<span class="w">   </span>├──<span class="w"> </span>index.html
│<span class="w">   </span>└──<span class="w"> </span>poor-mans-site.md
├──<span class="w"> </span>templates
│<span class="w">   </span>└──<span class="w"> </span>template.html
└──<span class="w"> </span>usernames
<span class="w">    </span>├──<span class="w"> </span>index.html
<span class="w">    </span>└──<span class="w"> </span>usernames.md
</code></pre></div>
<p>It opens every directory (except those in <code>nonposts</code>), runs pandoc on a Markdown file with the same name as the directory, and outputs <code>index.html</code> based on <code>templates/template.html</code>.</p>
<p>The top level <code>index.html</code> is not modified by the script at all. This page presumably would link to the subdirectories and must be manually modified. Ugh.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/the-poor-mans-static-site-generator</guid>
            <pubDate>2022-06-11</pubDate>
        </item>
        
        <item>
            <title>Collaborative Awe</title>
            <link>https://notes.zachmanson.com/collaborative-awe</link>
            
            <content:encoded>
                <![CDATA[<p>I recently received access to DALL-E and played around with it for a while. The interface is simple, just a textbox and a few curated images to give you some inspiration.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/dall-e.jpg" /></p>
<p>Once you submit a prompt to DALL-E, you are shown more curated images and their corresponding prompts while your request is processed.</p>
<p>DALL-E is capable of producing some amazing images. It takes a bit of warming up, but after little practice I was able to create wonderful and bizarre generative art. Despite this, I felt I didn't know how to produce something as interesting or engaging as their curated images the presented to me when using DALL-E. I could logically understand that the tool had potential, but I felt separated from it.</p>
<p>Using Midjourney is completely a different experience. Midjourney currently runs its generative art AI through a bot on their company Discord server. You generate images by typing in a command in certain channels, and all the images produced are visible to everyone on the server simultaneously. Before you can even type something you are assaulted by an unfiltered, non-curated array of incredible images, all diffusing right before your eyes.</p>
<video autoplay loop muted>
   <source src="/media/firehose.webm" type="video/webm">
</video>

<p>While both generators are impressive, using Midjourney was a visceral experience for me in a way that DALL-E was not. DALL-E is sipping a glass of water, Midjourney is a firehose blasting your face. I was immediately in awe.</p>
<p>The design of Midjourney makes it abundantly clear how big an impact this technology will have on the world. Watching hundreds of strangers generate images in real-time is jaw dropping. I was mesmerised, and within minutes saw images that moved me, despite being created by an AI without any intention behind them.</p>
<p>While I am sure both of these are the future, only one made me feel it.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/soldiers.jpg" /></p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/collaborative-awe</guid>
            <pubDate>2022-08-06</pubDate>
        </item>
        
        <item>
            <title>The Curse of the Last Good Day</title>
            <link>https://notes.zachmanson.com/the-curse-of-the-last-good-day</link>
            
            <description>Spoilers for The Fault in Our Stars, I guess</description>
            
            <content:encoded>
                <![CDATA[<p><em>The Fault in Our Stars</em> describes the phenomenon of the Last Good Day, a concept that has been echoing in my head ever since I read the book in high school. In the novel, the Last Good Day is the final time a person who is slowly deteriorating has a peaceful and pleasant moment with those around them.</p>
<blockquote>
<p>There's no way of knowing that your last good day is Your Last Good Day. At the time, it is just another good day.</p>
</blockquote>
<p>The story presents this in the context of Augustus' worsening condition and eventual death.</p>
<p>So far, I've been lucky enough to be free of facing slow health declines in myself and those close to me, but the concept of the Last Good Day has sat quietly in the back of my mind since first encountering it. Whenever I have some kind of impending loss, no matter how small, my thoughts tend to orbit around it.</p>
<p>With my bachelor's degree coming to a close soon, lots of my friends are moving away for careers or further study in other countries. The groups that have formed won't continue to exist in the way the have for the last 3 years. Not everyone is leaving of course and no one person is single load-bearing pillar of the group, but buildings tend not to handle losing half their supports.</p>
<p>I've been aware that this will happen for months, and at every group event in my head I'm constantly thinking <em>Is this going to be the Last Good Day? Is this Good enough? Can I make this better?</em> This is definitely not healthy and definitely doesn't help me enjoy the limited time left with my friends. This line of thinking is something I hesitate to even talk about with my friends in fear of inspiring the same kind of thought spirals in their minds.</p>
<p>Time has gone on and the erosion has begun. As I write this, another will be flying to Sydney tomorrow morning. Enough friends have left now that the Last Good Day has certainly passed, and yet I'm still not 100% sure which day it was. There's some strong contenders though.</p>
<p>That's life, I know I'll make more friends and there's no need to be melodramatic. Regardless, I'm going to miss this period and the people I've spent it with.</p>
<p>Cheers and auld lang syne and so on.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/the-curse-of-the-last-good-day</guid>
            <pubDate>2023-01-27</pubDate>
        </item>
        
        <item>
            <title>How Minecraft Stores Crafting Recipes</title>
            <link>https://notes.zachmanson.com/how-minecraft-stores-crafting-recipes</link>
            
            <description>Things noticed while stealing Mojang's work for marks</description>
            
            <content:encoded>
                <![CDATA[<p>As of Minecraft version 1.18.1, there are 739 crafting recipes in the game...depending on how you count. This little rabbithole is something I fell into during a meeting for a group project this week. The project tasks us with building a "daily puzzle game" in vein of Wordle, as my <a href="https://notes.zachmanson.com/web-development">web development</a> unit likes the idea of looking anachronistic in a few years.</p>
<p>The meeting was very preliminary, essentially just getting to know one another and bounce some ideas off each other. One idea I had been mulling over was "Wordle but with for Minecraft crafting recipes", which requires no explanation for players of these two games (which among my peers includes everyone). My group immediately latched onto the idea, but acknowledged the scope of including all Minecraft recipes would be challenge, both in terms of game design and actually acquiring them.</p>
<p>Transcribing by hand was possible but likely impractical, as was automatically scraping the <a href="https://minecraft.fandom.com/wiki/Minecraft_Wiki">wiki</a>. Luckily, the thousands of modders before us had poured through Minecraft's source file and through <a href="https://gaming.stackexchange.com/questions/382431/list-of-all-minecraft-crafting-recipes-for-crafting-web">cursory googling</a> we were able to get to <code>.minecraft/versions/1.18.1.jar/data/minecraft/recipes/</code>, where all crafting recipes in the game are stored as lovely little JSON files. Perfect!</p>
<p>Upon further inspection there are some mildly interesting quirks within these JSON files. The majority of recipes vaguely follow this format:</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
<span class="w">  </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:crafting_shaped&quot;</span><span class="p">,</span>
<span class="w">  </span><span class="nt">&quot;pattern&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&quot;###&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;#X#&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;#R#&quot;</span><span class="p">],</span>
<span class="w">  </span><span class="nt">&quot;key&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">&quot;R&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:redstone&quot;</span>
<span class="w">    </span><span class="p">},</span>
<span class="w">    </span><span class="nt">&quot;#&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:cobblestone&quot;</span>
<span class="w">    </span><span class="p">},</span>
<span class="w">    </span><span class="nt">&quot;X&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:bow&quot;</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">},</span>
<span class="w">  </span><span class="nt">&quot;result&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:dispenser&quot;</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>Seems like a straightforward and logical way of laying it all out. Presumably the "type" being "crafting_shaped" is to denote recipes with particular shapes (like a dispenser) as different from recipes where block arrangement doesn't matter (like mushroom stew).</p>
<p>Shapeless crafting recipes follow a simpler format:</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
<span class="w">  </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:crafting_shapeless&quot;</span><span class="p">,</span>
<span class="w">  </span><span class="nt">&quot;ingredients&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w">    </span><span class="p">{</span>
<span class="w">      </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:brown_mushroom&quot;</span>
<span class="w">    </span><span class="p">},</span>
<span class="w">    </span><span class="p">{</span>
<span class="w">      </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:red_mushroom&quot;</span>
<span class="w">    </span><span class="p">},</span>
<span class="w">    </span><span class="p">{</span>
<span class="w">      </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:bowl&quot;</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">],</span>
<span class="w">  </span><span class="nt">&quot;result&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:mushroom_stew&quot;</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>The recipes folder contains a lot more than two types though:</p>
<div class="highlight"><pre><span></span><code>zach@grannysmith<span class="w"> </span>recipes<span class="w"> </span>%<span class="w"> </span>awk<span class="w"> </span><span class="s1">&#39;FNR == 2&#39;</span><span class="w"> </span>*.json<span class="w"> </span><span class="p">|</span><span class="w"> </span>sort<span class="w"> </span><span class="p">|</span><span class="w"> </span>uniq<span class="w"> </span>-c
<span class="w">  </span><span class="m">24</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:blasting&quot;</span>,
<span class="w">   </span><span class="m">9</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:campfire_cooking&quot;</span>,
<span class="w"> </span><span class="m">543</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_shaped&quot;</span>,
<span class="w"> </span><span class="m">183</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_shapeless&quot;</span>,
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_armordye&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_bannerduplicate&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_bookcloning&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_firework_rocket&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_firework_star&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_firework_star_fade&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_mapcloning&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_mapextending&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_repairitem&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_shielddecoration&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_shulkerboxcoloring&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_suspiciousstew&quot;</span>
<span class="w">   </span><span class="m">1</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_tippedarrow&quot;</span>
<span class="w">  </span><span class="m">70</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:smelting&quot;</span>,
<span class="w">   </span><span class="m">9</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:smithing&quot;</span>,
<span class="w">   </span><span class="m">9</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:smoking&quot;</span>,
<span class="w"> </span><span class="m">198</span><span class="w">   </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stonecutting&quot;</span>,
</code></pre></div>
<p>Hold on, that's not just crafting recipes! All kinds "recipes" are stored in this directory, from normal smoking to smithing. This explains the repeated entries for items like cooked porkchop.</p>
<div class="highlight"><pre><span></span><code>zach@grannysmith<span class="w"> </span>recipes<span class="w"> </span>%<span class="w"> </span>ls<span class="w"> </span>cooked*.json
cooked_beef.json<span class="w">                           </span>cooked_mutton_from_smoking.json
cooked_beef_from_campfire_cooking.json<span class="w">     </span>cooked_porkchop.json
cooked_beef_from_smoking.json<span class="w">              </span>cooked_porkchop_from_campfire_cooking.json
cooked_chicken.json<span class="w">                        </span>cooked_porkchop_from_smoking.json
cooked_chicken_from_campfire_cooking.json<span class="w">  </span>cooked_rabbit.json
cooked_chicken_from_smoking.json<span class="w">           </span>cooked_rabbit_from_campfire_cooking.json
cooked_cod.json<span class="w">                            </span>cooked_rabbit_from_smoking.json
cooked_cod_from_campfire_cooking.json<span class="w">      </span>cooked_salmon.json
cooked_cod_from_smoking.json<span class="w">               </span>cooked_salmon_from_campfire_cooking.json
cooked_mutton.json<span class="w">                         </span>cooked_salmon_from_smoking.json
cooked_mutton_from_campfire_cooking.json
</code></pre></div>
<p>Smelted items have an even simpler format:</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
<span class="w">  </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:smelting&quot;</span><span class="p">,</span>
<span class="w">  </span><span class="nt">&quot;ingredient&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">&quot;item&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:porkchop&quot;</span>
<span class="w">  </span><span class="p">},</span>
<span class="w">  </span><span class="nt">&quot;result&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;minecraft:cooked_porkchop&quot;</span><span class="p">,</span>
<span class="w">  </span><span class="nt">&quot;experience&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.35</span><span class="p">,</span>
<span class="w">  </span><span class="nt">&quot;cookingtime&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span>
<span class="p">}</span>
</code></pre></div>
<p>Puzzingly there is a value for cooking time for each item, though smelting time is dependent on the type of furnace used rather than the item being smelted.</p>
<p>For example, all items cooked in a blast furnace have the same value for cooking time:</p>
<div class="highlight"><pre><span></span><code>zach@grannysmith<span class="w"> </span>recipes<span class="w"> </span>%<span class="w"> </span>cat<span class="w"> </span>*blast*<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s2">&quot;cookingtime&quot;</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
<span class="w">  </span><span class="s2">&quot;cookingtime&quot;</span>:<span class="w"> </span><span class="m">100</span>
</code></pre></div>
<p>It could be to accommodate varying smelting times in the future, but for the moment this seems redundant.</p>
<p>All of the special crafting recipes aren't stored at all, which makes sense since these are more complex than what should be written in a single JSON file.</p>
<div class="highlight"><pre><span></span><code>zach@grannysmith<span class="w"> </span>recipes<span class="w"> </span>%<span class="w"> </span>cat<span class="w"> </span><span class="k">$(</span>grep<span class="w"> </span>-lr<span class="w"> </span>.<span class="w"> </span>-e<span class="w"> </span><span class="s2">&quot;special&quot;</span><span class="k">)</span>
<span class="o">{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_bookcloning&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_firework_star_fade&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_firework_rocket&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_shulkerboxcoloring&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_mapcloning&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_repairitem&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_bannerduplicate&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_armordye&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_tippedarrow&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_firework_star&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_mapextending&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_shielddecoration&quot;</span>
<span class="o">}{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_special_suspiciousstew&quot;</span>
<span class="o">}</span>
</code></pre></div>
<p>Tools smithed with netherite get a different and simple format as well:</p>
<div class="highlight"><pre><span></span><code>&gt;<span class="w"> </span>cat<span class="w"> </span>netherite_sword_smithing.json
<span class="o">{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:smithing&quot;</span>,
<span class="w">  </span><span class="s2">&quot;base&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:diamond_sword&quot;</span>
<span class="w">  </span><span class="o">}</span>,
<span class="w">  </span><span class="s2">&quot;addition&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:netherite_ingot&quot;</span>
<span class="w">  </span><span class="o">}</span>,
<span class="w">  </span><span class="s2">&quot;result&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:netherite_sword&quot;</span>
<span class="w">  </span><span class="o">}</span>
<span class="o">}</span>
</code></pre></div>
<p>Stonecutting unveiled an interesting quirk:</p>
<div class="highlight"><pre><span></span><code>&gt;<span class="w"> </span>cat<span class="w"> </span>chiseled_stone_bricks_stone_from_stonecutting.json
<span class="o">{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stonecutting&quot;</span>,
<span class="w">  </span><span class="s2">&quot;ingredient&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone&quot;</span>
<span class="w">  </span><span class="o">}</span>,
<span class="w">  </span><span class="s2">&quot;result&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:chiseled_stone_bricks&quot;</span>,
<span class="w">  </span><span class="s2">&quot;count&quot;</span>:<span class="w"> </span><span class="m">1</span>
<span class="o">}</span>
</code></pre></div>
<p>The count value determine how many items are output by the recipe, and appears in over 579 total recipes. For the standard crafting recipes this value only appears for recipes that output more than 1 item, but curiously stonecutter recipes always include this value.</p>
<p>All this elicited a nose exhale and an "oh neat" from me as I poked around the files. Most of these details aren't important to the project, but the parts that are important will be a huge time save. Web scraping in my experience is a pain (though somehow I think the maintainers of the Minecraft wiki would be more consistent than the <a href="https://github.com/pavo-etc/wa-covid-tracker/blob/1e1e8e317084e22aba6c143afbe9a8249e6231dc/scraper.py#L29">WA Government</a>).</p>
<p>Now we can focus on the real trouble - every other aspect of puzzle game design and web development. I am particularly not looking forward to dealing with the interaction between Wordle-style position hints and different crafting orientation/positions.</p>
<hr />
<p><strong>Update 2022-06-05:</strong></p>
<p><a href="https://notes.zachmanson.com/minecraftle">Minecraftle</a> is <a href="https://minecraftle.zachmanson.com">playable</a> and nearly fully finished. We submitted two weeks ago but there are a few things I want to implement for my own interest. The JSON files discussed in this post were used, but their format was inconvenient for querying so needed to be restructured. Yesterday I rewrote the script to restructure the recipe JSON files since the initial version was a mess, and finally implemented the functions to validate recipes on object placement. These changes revealed a few more quirks in the recipe JSON files.</p>
<p>Some recipes have multiple forms, such as torches which use coal or charcoal. I had assumed this would just result in two seperate recipe files, but:</p>
<div class="highlight"><pre><span></span><code>&gt;<span class="w"> </span>cat<span class="w"> </span>torch.json
<span class="o">{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_shaped&quot;</span>,
<span class="w">  </span><span class="s2">&quot;pattern&quot;</span>:<span class="w"> </span><span class="o">[</span>
<span class="w">    </span><span class="s2">&quot;X&quot;</span>,
<span class="w">    </span><span class="s2">&quot;#&quot;</span>
<span class="w">  </span><span class="o">]</span>,
<span class="w">  </span><span class="s2">&quot;key&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="s2">&quot;#&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">      </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stick&quot;</span>
<span class="w">    </span><span class="o">}</span>,
<span class="w">    </span><span class="s2">&quot;X&quot;</span>:<span class="w"> </span><span class="o">[</span>
<span class="w">      </span><span class="o">{</span>
<span class="w">        </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:coal&quot;</span>
<span class="w">      </span><span class="o">}</span>,
<span class="w">      </span><span class="o">{</span>
<span class="w">        </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:charcoal&quot;</span>
<span class="w">      </span><span class="o">}</span>
<span class="w">    </span><span class="o">]</span>
<span class="w">  </span><span class="o">}</span>,
<span class="w">  </span><span class="s2">&quot;result&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:torch&quot;</span>,
<span class="w">    </span><span class="s2">&quot;count&quot;</span>:<span class="w"> </span><span class="m">4</span>
<span class="w">  </span><span class="o">}</span>
<span class="o">}</span>
</code></pre></div>
<p>This actually caused a mild issue in Minecraftle that I currently have a hacky solution for. As far as I can tell this doesn't appear in many recipes (though I'm too lazy to write the regex to check). When we restructure the JSON files, we simplify the format so the <code>pattern</code> field contains the actual item names rather than character representations. This doesn't leave much room alternate forms, short of just repeating the whole recipe with the differing items. While totally possible, I just forced it to select the first item whenever there are multiple options. This means that you would be unable to craft a torch with charcoal.</p>
<p>As far as I can tell this poses no issues since in Minecraftle's current form you don't receive charcoal as a crafting ingredient, and I am yet to find another appearance of this format that uses the items in <code>given_ingredients.json</code>. I'm still uncomfortable with this though, and I hope to come back and fix it after exams.</p>
<p>Another oddity reared its head with stone tools:</p>
<div class="highlight"><pre><span></span><code>&gt;<span class="w"> </span>cat<span class="w"> </span>stone_axe.json
<span class="o">{</span>
<span class="w">  </span><span class="s2">&quot;type&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crafting_shaped&quot;</span>,
<span class="w">  </span><span class="s2">&quot;pattern&quot;</span>:<span class="w"> </span><span class="o">[</span>
<span class="w">    </span><span class="s2">&quot;XX&quot;</span>,
<span class="w">    </span><span class="s2">&quot;X#&quot;</span>,
<span class="w">    </span><span class="s2">&quot; #&quot;</span>
<span class="w">  </span><span class="o">]</span>,
<span class="w">  </span><span class="s2">&quot;key&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="s2">&quot;#&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">      </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stick&quot;</span>
<span class="w">    </span><span class="o">}</span>,
<span class="w">    </span><span class="s2">&quot;X&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_tool_materials&quot;</span>
<span class="w">    </span><span class="o">}</span>
<span class="w">  </span><span class="o">}</span>,
<span class="w">  </span><span class="s2">&quot;result&quot;</span>:<span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="s2">&quot;item&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_axe&quot;</span>
<span class="w">  </span><span class="o">}</span>
<span class="o">}</span>
</code></pre></div>
<p>See it? <code>tag</code> replaces <code>item</code> in a number of cases, such as stone tools. Blackstone and cobbled deepslate have been added fairly recently to the game, and can be used for interchangably for cobblestone in a number of recipes. This kind of notation appears in a number of recipes:</p>
<div class="highlight"><pre><span></span><code>&gt;<span class="w"> </span>grep<span class="w">  </span><span class="s2">&quot;\&quot;tag\&quot;:&quot;</span><span class="w"> </span>*
acacia_planks.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:acacia_logs&quot;</span>
barrel.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
barrel.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:wooden_slabs&quot;</span>
beehive.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
birch_planks.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:birch_logs&quot;</span>
black_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
blue_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
bookshelf.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
bowl.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
brewing_stand.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_crafting_materials&quot;</span>
brown_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
campfire.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:logs&quot;</span>
campfire.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:coals&quot;</span>
cartography_table.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
chest.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
composter.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:wooden_slabs&quot;</span>
crafting_table.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
crimson_planks.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:crimson_stems&quot;</span>
cyan_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
dark_oak_planks.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:dark_oak_logs&quot;</span>
daylight_detector.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:wooden_slabs&quot;</span>
fletching_table.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
furnace.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_crafting_materials&quot;</span>
gray_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
green_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
grindstone.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
jukebox.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
jungle_planks.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:jungle_logs&quot;</span>
lectern.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:wooden_slabs&quot;</span>
light_blue_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
light_gray_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
lime_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
loom.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
magenta_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
note_block.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
oak_planks.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:oak_logs&quot;</span>
orange_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
painting.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:wool&quot;</span>
pink_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
piston.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
purple_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
red_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
shield.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
smithing_table.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
smoker.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:logs&quot;</span>
soul_campfire.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:logs&quot;</span>
soul_campfire.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:soul_fire_base_blocks&quot;</span>
soul_torch.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:soul_fire_base_blocks&quot;</span>
spruce_planks.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:spruce_logs&quot;</span>
stick.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
stone_axe.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_tool_materials&quot;</span>
stone_hoe.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_tool_materials&quot;</span>
stone_pickaxe.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_tool_materials&quot;</span>
stone_shovel.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_tool_materials&quot;</span>
stone_sword.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:stone_tool_materials&quot;</span>
tripwire_hook.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
warped_planks.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:warped_stems&quot;</span>
white_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
wooden_axe.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
wooden_hoe.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
wooden_pickaxe.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
wooden_shovel.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
wooden_sword.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
yellow_bed.json:<span class="w">      </span><span class="s2">&quot;tag&quot;</span>:<span class="w"> </span><span class="s2">&quot;minecraft:planks&quot;</span>
</code></pre></div>
<p>These tags are referring to <code>.minecraft/versions/1.18.1.jar/data/minecraft/tags/items/</code>, which holds yet more JSON files with lists of all items that belong to each tag. For example:</p>
<div class="highlight"><pre><span></span><code>&gt;<span class="w"> </span>cat<span class="w"> </span>stone_crafting_materials.json
<span class="o">{</span>
<span class="w">  </span><span class="s2">&quot;replace&quot;</span>:<span class="w"> </span>false,
<span class="w">  </span><span class="s2">&quot;values&quot;</span>:<span class="w"> </span><span class="o">[</span>
<span class="w">    </span><span class="s2">&quot;minecraft:cobblestone&quot;</span>,
<span class="w">    </span><span class="s2">&quot;minecraft:blackstone&quot;</span>,
<span class="w">    </span><span class="s2">&quot;minecraft:cobbled_deepslate&quot;</span>
<span class="w">  </span><span class="o">]</span>
<span class="o">}</span>
</code></pre></div>
<p>To deal with these I've currently just put edge cases in to handle these tags manually, though I would like to have a more robust solution in the future that can be determined by <code>given_ingredients.json</code>.</p>
<p>Both of these appear to be solving the same problem, and it's strange that coal doesn't use the <code>tag</code> field, especially since there is <code>tag</code> JSON file that seems made for this:</p>
<div class="highlight"><pre><span></span><code>&gt;<span class="w"> </span>cat<span class="w"> </span>coals.json
<span class="o">{</span>
<span class="w">  </span><span class="s2">&quot;replace&quot;</span>:<span class="w"> </span>false,
<span class="w">  </span><span class="s2">&quot;values&quot;</span>:<span class="w"> </span><span class="o">[</span>
<span class="w">    </span><span class="s2">&quot;minecraft:coal&quot;</span>,
<span class="w">    </span><span class="s2">&quot;minecraft:charcoal&quot;</span>
<span class="w">  </span><span class="o">]</span>
<span class="o">}</span>
</code></pre></div>
<p>I theorize that the torch was older method of achieving this goal and just hasn't been updated to use the new system (though I am again too lazy and won't check old versions), unless there is some other difference I am not aware of between the two methods. Maybe I should send Mojang a pull request.</p>
<p>Recipe validation was the final feature I really felt was necessary for my own sense of completion. That said, we never added shapeless recipe support... or varying difficulties... or hardcore mode. Oh well.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/how-minecraft-stores-crafting-recipes</guid>
            <pubDate>2022-03-31</pubDate>
        </item>
        
        <item>
            <title>RTK Query vs. Infinite Scrolling</title>
            <link>https://notes.zachmanson.com/rtk-query-vs.-infinite-scrolling</link>
            
            <content:encoded>
                <![CDATA[<p><strong>Update 2026-02-04: There is now native support for infinite scrolling-style loading patterns.</strong> See the <a href="https://github.com/reduxjs/redux-toolkit/releases/tag/v2.6.0">announcement here</a> and the <a href="https://redux-toolkit.js.org/rtk-query/usage/infinite-queries">docs here</a>.</p>
<hr />
<p>RTK Query with <a href="https://notes.zachmanson.com/react">React</a> is pretty great! The primary pattern you find yourself using with it is: </p>
<ol>
<li>writing a small API definition that provides <ol>
<li>the endpoint url </li>
<li>type definitions for request args and response schema</li>
<li>(usually) simply logic for converting passed argument type into request params/body</li>
<li>(usually) simple logic for converting response payload into the response schema</li>
</ol>
</li>
<li>importing a hook that RTK Query automatically generates for you</li>
<li>reuse the same hook across multiple components</li>
</ol>
<p>RTK Query handles loading states, stale while revalidate states, error states, type safety, and caching.</p>
<p>When writing React components, the caching is the most impactful of these.   You can just call the same hook in many different components, and as long as the params are the same the same it will only be a single network request. This removes the need for many invocations <code>useState</code> and Redux state calls, since you can just feign a call to the backend every time you need that data.</p>
<p>There is a little boilerplate you have to write, but the meat of a basic RTK Query definition will look something like this:</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">ids</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/location&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">},</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>This will generate a hook called <code>useGetLocation</code> which will be cached.  Suddenly you never need to store a <code>Location</code> object in state to share it across components and you can just call <code>useGetLocation({id:5})</code> every time you need it. It also provides a lazy version of the hook that returns a normal function you can use to retrieve the data, which is good for APIs that need to be called multiple times.</p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">LocationCard</span><span class="p">({</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="nx">number</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="nx">location</span><span class="p">,</span><span class="w"> </span><span class="nx">isLoading</span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useGetLocation</span><span class="p">({</span><span class="nx">id</span><span class="o">:</span><span class="nx">id</span><span class="p">});</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">getLocationsLazy</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useLazyGetLocation</span><span class="p">();</span>
<span class="w">    </span><span class="p">...</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">{</span><span class="nx">location</span><span class="p">.</span><span class="nx">address</span><span class="p">}</span>
<span class="w">        </span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">    </span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>For straightforward GET requests this pattern stellar.  RTK Query encourages you to put all the API definitions into a single file (<code>src/api/api.ts</code>), or into a small number files with particular scopes.  For small-medium applications this works well, if you know the URL of an endpoint you want to hit you can just search within the <code>api.ts</code> file, use the accompanying hook and you are good to go, type safety and all.  I'm sure this single file approach would breakdown for big applications but its very pleasant at smaller scales.</p>
<p>RTK Query also lets you do POST/PUT/PATCH requests with a similar definition style. Instead of using <code>build.query</code>, you use <code>build.mutation</code>.</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">addLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.mutation</span><span class="o">&lt;</span>
<span class="w">      </span><span class="p">{</span><span class="w"> </span><span class="nx">location_id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span><span class="w"> </span><span class="nx">site_id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span><span class="w"> </span><span class="nx">address</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span><span class="w"> </span><span class="nx">abn</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span><span class="w"> </span><span class="nx">user_id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">},</span>
<span class="w">      </span><span class="nx">NewLocation</span>
<span class="w">    </span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">body</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/location&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;POST&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">body</span><span class="o">:</span><span class="w"> </span><span class="kt">body</span><span class="p">,</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">    </span><span class="p">}),</span>
<span class="w">    </span><span class="nx">patchLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.mutation</span><span class="o">&lt;</span><span class="ow">void</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">Partial</span><span class="o">&lt;</span><span class="nx">Location</span><span class="o">&gt;</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;PATCH&quot;</span><span class="p">,</span>
<span class="w">          </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="sb">`api/v1/location/</span><span class="si">${</span><span class="nx">primaryKey</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
<span class="w">          </span><span class="nx">body</span><span class="o">:</span><span class="w"> </span><span class="kt">data</span><span class="p">,</span>
<span class="w">        </span><span class="p">};</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>The hooks generated here will return a function that can be used to send requests.</p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">LocationCard</span><span class="p">({</span><span class="nx">id</span><span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">:</span><span class="nx">number</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="nx">location</span><span class="p">,</span><span class="w"> </span><span class="nx">isLoading</span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useGetLocationQuery</span><span class="p">({</span><span class="nx">id</span><span class="o">:</span><span class="nx">id</span><span class="p">})</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">patchLocation</span><span class="p">,</span><span class="w"> </span><span class="nx">isPatchLoading</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePatchLocationMutation</span><span class="p">()</span>

<span class="w">    </span><span class="kd">function</span><span class="w"> </span><span class="nx">onUpdate</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="o">:</span><span class="nx">Partial</span><span class="p">&lt;</span><span class="nt">Location</span><span class="p">&gt;)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">patchLocation</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="p">...</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">{</span><span class="nx">location</span><span class="p">.</span><span class="nx">address</span><span class="p">}</span>
<span class="w">            </span><span class="p">...</span>
<span class="w">            </span><span class="p">{</span><span class="cm">/* some input that calls onUpdate on change */</span><span class="p">}</span>
<span class="w">        </span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">    </span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div>
<p>But wait! If I patch a location, how will the useGetLocation hook know that it's cache is invalid?</p>
<h2 id="cache-invalidation">Cache Invalidation</h2>
<p>RTK Query uses a tag system for cache invalidation, where a tag represents a kind of query.  Queries can assign themselves tags, and mutations can provide a list of tags they will invalidate. </p>
<p>These can be static</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">ids</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/location&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">},</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;Location&#39;</span><span class="p">],</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>or dynamic, based on the params given</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">ids</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/location&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">},</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">id</span><span class="p">}]</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>And similarly for mutations</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">patchLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.mutation</span><span class="o">&lt;</span><span class="ow">void</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">Partial</span><span class="o">&lt;</span><span class="nx">Location</span><span class="o">&gt;</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;PATCH&quot;</span><span class="p">,</span>
<span class="w">          </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="sb">`api/v1/location/</span><span class="si">${</span><span class="nx">primaryKey</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
<span class="w">          </span><span class="nx">body</span><span class="o">:</span><span class="w"> </span><span class="kt">data</span><span class="p">,</span>
<span class="w">        </span><span class="p">};</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">invalidatesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="kr">type</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Location&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">arg.id</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="c1">// or just [&quot;Location&quot;] to invalidate the whole category</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>Where you can, dynamic tags are generally better since you have much more granular control.  Again, this works pretty well for straightforward queries.</p>
<h2 id="pagination">Pagination</h2>
<p>RTK Query doesn't include a mechanism for paging data, but passing a page number as a param works fine if you have discrete pages.</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocations</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/locations&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="p">}</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[...</span><span class="nx">result</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">location</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">location.id</span><span class="p">}),</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;LIST&quot;</span><span class="p">}],</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<h2 id="but-what-about-infinite-scroll">But what about infinite scroll?</h2>
<p>When infinitely scrolling, normally you'd have an array that stores all the results that have been loaded so far, and then you'd append new results to the end of this list when they are loaded in (maybe even dropping some entries from the start if you are memory constrained).  The hook generated by the previous example will only ever let you access one page's entries at a time. </p>
<p>RTK Query doesn't have any special functions for infinite scrolling, but the pieces it gives you are enough to make it work. When writing a query definition you can explicitly set which params contribute are used to define unique cache entries, and another function to say how old cache entries should be overwritten by new ones.</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocations</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/locations&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="p">}</span>

<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[...</span><span class="nx">result</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">location</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">location.id</span><span class="p">}),</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;LIST&quot;</span><span class="p">}],</span>

<span class="w">      </span><span class="c1">// overwrite cached value when page value changes, treat page</span>
<span class="w">      </span><span class="nx">serializeQueryArgs</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">queryArgs</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="p">...</span><span class="nx">queryArgs</span><span class="p">,</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">undefined</span><span class="w"> </span><span class="p">}),</span>

<span class="w">      </span><span class="c1">// when overwriting cache value, append new data to old list</span>
<span class="w">      </span><span class="nx">merge</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">currentCache</span><span class="p">,</span><span class="w"> </span><span class="nx">newData</span><span class="p">,</span><span class="w"> </span><span class="nx">otherArgs</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="nx">currentCache</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">push</span><span class="p">(...</span><span class="nx">newData</span><span class="p">);</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>This works riiiiight until you need to invalidate the cache. Say you have a page with infinite scroll via pressing a load more button at the bottom, and the user has pressed it 3 times. At this stage, the hook will look like <code>const {data} = useGetLocationsQuery({page: 4});</code>. If page size is 20, data, will contain 80 entries. Then you edit an entry that appears on page 3. Or you add a new location.  Both of these operation will invalidate the cache, but the merge strategy provided will simply add the new cache to the end of the previous cache, so  <code>const {data} = useGetLocationsQuery({page: 4});</code> will now have 100 entries, 80 from the original 4 pages and 20 from the 4th page repeated again.  <strong>Ideally the merge function would have a way of distinguishing cache updates that are due to tag invalidation from cache updates due to parameter changes, but there is currently no way to do this</strong>.</p>
<h2 id="workarounds">Workarounds</h2>
<p>There are a <a href="https://github.com/reduxjs/redux-toolkit/discussions/1163">number</a> <a href="https://github.com/reduxjs/redux-toolkit/issues/3733">of</a> <a href="https://github.com/reduxjs/redux-toolkit/discussions/3174">discussion</a> of how to mitigate this.  The solution I went with looks like this.</p>
<p>Firstly, the response payload must include the page number metadata.  This is usually the case with paginated endpoints, but this data now needs to be included in the cached result in the query definition.</p>
<p>When merging the cache, only append the new data if the new data page number is greater than the old value:</p>
<div class="highlight"><pre><span></span><code><span class="kr">type</span><span class="w"> </span><span class="nx">PaginatedData</span><span class="o">&lt;</span><span class="nx">ItemType</span><span class="o">&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
<span class="w">  </span><span class="nx">pages</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
<span class="w">  </span><span class="nx">per_page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
<span class="w">  </span><span class="nx">total</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
<span class="w">  </span><span class="nx">items</span><span class="o">:</span><span class="w"> </span><span class="kt">ItemType</span><span class="p">[];</span>
<span class="p">};</span>
<span class="p">...</span>

<span class="w">    </span><span class="nx">getLocations</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">PaginatedData</span><span class="o">&lt;</span><span class="nx">Location</span><span class="o">&gt;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/locations&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="p">}</span>

<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[...</span><span class="nx">result</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">location</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">location.id</span><span class="p">}),</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;LIST&quot;</span><span class="p">}],</span>

<span class="w">      </span><span class="c1">// overwrite cached value when page value changes, treat page</span>
<span class="w">      </span><span class="nx">serializeQueryArgs</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">queryArgs</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="p">...</span><span class="nx">queryArgs</span><span class="p">,</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">undefined</span><span class="w"> </span><span class="p">}),</span>

<span class="w">      </span><span class="c1">// when overwriting cache value, append new data to old list if page value increases</span>
<span class="w">      </span><span class="nx">merge</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">currentCache</span><span class="p">,</span><span class="w"> </span><span class="nx">newData</span><span class="p">,</span><span class="w"> </span><span class="nx">otherArgs</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">newData</span><span class="p">.</span><span class="nx">page</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="nx">currentCache</span><span class="p">.</span><span class="nx">page</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="nx">currentCache</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">push</span><span class="p">(...</span><span class="nx">newData</span><span class="p">.</span><span class="nx">items</span><span class="p">);</span>
<span class="w">          </span><span class="nx">currentCache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">...</span><span class="nx">newData</span><span class="p">,</span><span class="w"> </span><span class="nx">items</span><span class="o">:</span><span class="w"> </span><span class="kt">currentCache.items</span><span class="w"> </span><span class="p">};</span>
<span class="w">        </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="k">return</span><span class="w"> </span><span class="nx">newData</span><span class="p">;</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>Then in the component where the hook is called, ensure that the page number is reset to 1 whenever a mutation occurs that will invalidate the <code>useGetLocationsQuery</code> data.  Since this page isn't an increase from the previous, the old cache will be discarded. This can be annoying since the component that triggers the mutation and invalidates the data may be completely seperate from the component that queries for the data.  </p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">EditLocationAddress</span><span class="p">({</span><span class="nx">id</span><span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">:</span><span class="kt">number</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">patchLocation</span><span class="p">,</span><span class="w"> </span><span class="nx">isPatchLoading</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePatchLocationMutation</span><span class="p">();</span>

<span class="w">    </span><span class="kd">function</span><span class="w"> </span><span class="nx">onUpdate</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="o">:</span><span class="kt">Partial</span><span class="p">&lt;</span><span class="nt">Location</span><span class="p">&gt;)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">patchLocation</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="p">);</span>
<span class="w">        </span><span class="c1">// TODO somehow reset the page number to 0 in all relevant places</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="p">...</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">{</span><span class="cm">/* some input that calls onUpdate on change */</span><span class="p">}</span>
<span class="w">        </span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">    </span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>There is probably a clever way of doing this where the cached page number is used as the canonical page number across all components, and then you could force a reset back to one from any component, but I haven't needed to implement this just yet in any applications.</p>
<p>Maybe something like this? I haven't tried it yet.</p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">EditLocationAddress</span><span class="p">({</span><span class="nx">id</span><span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">:</span><span class="nx">number</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">patchLocation</span><span class="p">,</span><span class="w"> </span><span class="nx">isPatchLoading</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePatchLocationMutation</span><span class="p">();</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">getLocationsLazy</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useLazyGetLocationsQuery</span><span class="p">();</span>

<span class="w">    </span><span class="kd">function</span><span class="w"> </span><span class="nx">onUpdate</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="o">:</span><span class="nx">Partial</span><span class="p">&lt;</span><span class="nt">Location</span><span class="p">&gt;)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">patchLocation</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="p">);</span>
<span class="w">        </span><span class="nx">getLocationsLazy</span><span class="p">(</span>
<span class="w">            </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="p">},</span>
<span class="w">            </span><span class="kc">true</span><span class="w"> </span><span class="c1">// lazy queries ignore cache by default, true overrides this</span>
<span class="w">        </span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="p">...</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">{</span><span class="cm">/* some input that calls onUpdate on change */</span><span class="p">}</span>
<span class="w">        </span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">    </span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/rtk-query-vs.-infinite-scrolling</guid>
            <pubDate>2024-06-03</pubDate>
        </item>
        
        <item>
            <title>LinkedIn's Ghost Text</title>
            <link>https://notes.zachmanson.com/linkedins-ghost-text</link>
            
            <description>Why does LinkedIn copy text twice?</description>
            
            <content:encoded>
                <![CDATA[<p>Why does LinkedIn do this?</p>
<video autoplay loop muted>
   <source src="/media/linkedin-duplication.webm" type="video/webm">
</video>

<p>If you look into the source of a LinkedIn profile page (at your own personal risk) you'll find a text element like this</p>
<p><img alt="" src="https://notes.zachmanson.com/media/linkedin-element.png" /></p>
<p>will have HTML like this</p>
<div class="highlight"><pre><span></span><code><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;display-flex flex-row justify-space-between&quot;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;</span>
<span class="s">          display-flex flex-column full-width&quot;</span><span class="p">&gt;</span>

        <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;display-flex flex-wrap align-items-center full-height&quot;</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;mr1 t-bold&quot;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span> <span class="na">aria-hidden</span><span class="o">=</span><span class="s">&quot;true&quot;</span><span class="p">&gt;</span><span class="cm">&lt;!----&gt;</span>Product manager, Creative coder<span class="cm">&lt;!----&gt;</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;visually-hidden&quot;</span><span class="p">&gt;</span><span class="cm">&lt;!----&gt;</span>Product manager, Creative coder<span class="cm">&lt;!----&gt;</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="cm">&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;</span>
        <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;t-14 t-normal&quot;</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">span</span> <span class="na">aria-hidden</span><span class="o">=</span><span class="s">&quot;true&quot;</span><span class="p">&gt;</span><span class="cm">&lt;!----&gt;</span>Freelancer/Consultant<span class="cm">&lt;!----&gt;</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;visually-hidden&quot;</span><span class="p">&gt;</span><span class="cm">&lt;!----&gt;</span>Freelancer/Consultant<span class="cm">&lt;!----&gt;</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;t-14 t-normal t-black--light&quot;</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">span</span> <span class="na">aria-hidden</span><span class="o">=</span><span class="s">&quot;true&quot;</span><span class="p">&gt;</span><span class="cm">&lt;!----&gt;</span>2009 - 2018 · 9 yrs<span class="cm">&lt;!----&gt;</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;visually-hidden&quot;</span><span class="p">&gt;</span><span class="cm">&lt;!----&gt;</span>2009 - 2018 · 9 yrs<span class="cm">&lt;!----&gt;</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;t-14 t-normal t-black--light&quot;</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">span</span> <span class="na">aria-hidden</span><span class="o">=</span><span class="s">&quot;true&quot;</span><span class="p">&gt;</span><span class="cm">&lt;!----&gt;</span>Europe<span class="cm">&lt;!----&gt;</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;visually-hidden&quot;</span><span class="p">&gt;</span><span class="cm">&lt;!----&gt;</span>Europe<span class="cm">&lt;!----&gt;</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>

    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

    <span class="cm">&lt;!----&gt;</span>
    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;pvs-entity__action-container&quot;</span><span class="p">&gt;</span>
        <span class="cm">&lt;!----&gt;</span>
    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div>
<p>Gross.  Why the hidden duplicated text?  Why the empty comments?  Why seperate aria versions of text?  Is this related to using Ember?  LinkedIn has so many little unpleasantries like this.</p>
<hr />
<h2 id="2024-update-linkedin-still-shit">2024 Update: LinkedIn Still Shit</h2>
<p>Still here.  Still broken. Still annoying</p>
<video autoplay loop muted>
   <source src="/media/linkedin-duplication-2.webm" type="video/webm">
</video>

<h2 id="2025-update-slack-also-does-this">2025 Update: Slack also does this</h2>
<p>When you highlight, copy and paste a name.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/linkedins-ghost-text</guid>
            <pubDate>2023-04-16</pubDate>
        </item>
        
        <item>
            <title>XZ</title>
            <link>https://notes.zachmanson.com/xz</link>
            
            <description>The most advanced supply chain attack ever found.</description>
            
            <content:encoded>
                <![CDATA[<p>The XZ exploit is a clever supply chain attack, placing a RCE in sshd using a vulnerability in XZ, a compression library that many <a href="https://notes.zachmanson.com/linux">Linux</a> distributions call when running OpenSSH (sshd -&gt; libsystemd (for systemd notifications) -&gt; liblzma (for logging)).  It was discovered by Andres Freund when he noticed that sshd was running with abnormally high CPU usage.</p>
<p>The vulnerability was submitted by Jia Tan, a maintainer of XZ project who had been an active contributor since 2021.   The vulnerability was extremely obfuscated.  <a href="https://www.openwall.com/lists/oss-security/2024/03/29/4">Read the original backdoor report by Andres Freund for more details on this</a>.</p>
<h2 id="maintainers">Maintainers</h2>
<p>There are two maintainers of the XZ projects, Jia Tan and Lasse Collin.  Lasse Collin is the original author of the XZ Utils library, whereas Jia Tan has only been a maintainer since 2022.</p>
<p>Tan appears so have little to no online footprint prior to 2021 when they started making useful contributions to XZ.  It is speculated that Jia Tan is an invented persona.</p>
<blockquote>
<p>He has been part of the xz project for 2 years, adding all sorts of binary test files, and to be honest with this level of sophistication I would be suspicious of even older versions of xz until proven otherwise.</p>
</blockquote>
<p><cite class="standalone"><a href="https://news.ycombinator.com/item?id=39866275">Richard Jones</a></cite></p>
<ul>
<li><a href="https://robmensching.com/blog/posts/2024/03/30/a-microcosm-of-the-interactions-in-open-source-projects/">A Microcosm of the interactions in Open Source projects</a>, and interesting analysis of mailing list discussions about Jia Tan's contributions to the XZ project before Tan was made maintainer</li>
<li><a href="https://tukaani.org/xz-backdoor/">Lasse Collin's page</a> tracking the vulnerability</li>
</ul>
<h2 id="features">Features</h2>
<ul>
<li>kill switch based on <a href="https://piaille.fr/@zeno/112185928685603910">environment variable</a><ul>
<li><code>yolAbejyiejuvnup=Evjtgvsh5okmkAvj</code> is hardcoded (and then obfuscated)</li>
</ul>
</li>
<li>backdoor itself is secured as it checks for a private key<ul>
<li>this is an interesting argument against "when you make a backdoor for good guys you also make a backdoor for bad guys" though that's another conversation entirely</li>
</ul>
</li>
</ul>
<h2 id="oddities">Oddities</h2>
<ul>
<li>In 2022 <a href="https://www.mail-archive.com/xz-devel@tukaani.org/msg00566.html">there were other suspected sock puppet accounts pushing Collin to introduce new maintainers</a></li>
<li>Tan disabled a function in <a href="https://github.com/google/oss-fuzz/pull/10667">oss-fuzzer</a> in 2023 that the exploit relied on that may have revealed the exploit (<a href="https://social.treehouse.systems/@Aissen/112180302735030319">source</a>)</li>
<li>the day before the exploit was found the xz-java project added a SECURITY.md file </li>
</ul>
<blockquote>
<p>If you discover a security vulnerability in this project please report it privately. <em>Do not disclose it as a public issue.</em> This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released.</p>
</blockquote>
<ul>
<li>It disabled Linux landlock?<ul>
<li><a href="https://git.tukaani.org/?p=xz.git;a=blobdiff;f=CMakeLists.txt;h=d2b1af7ab0ab759b6805ced3dff2555e2a4b3f8e;hp=76700591059711e3a4da5b45cf58474dac4e12a7;hb=328c52da8a2bbb81307644efdb58db2c422d9ba7;hpb=eb8ad59e9bab32a8d655796afd39597ea6dcc64d">This commit</a> introduces a syntax error that prevents Linux landlock from being activated.  The test program designed to test if landlock was supported would always fail due to this errant <code>.</code>.</li>
</ul>
</li>
<li>Tan's first GitHub PR is to libarchive and is suspect as it subtly replaces <code>safe_printf</code> calls with unsafe <code>fprintf</code></li>
</ul>
<h2 id="discovery">Discovery</h2>
<blockquote>
<p>I was doing some micro-benchmarking at the time, needed to quiesce the system to reduce noise. Saw sshd processes were using a surprising amount of CPU, despite immediately failing because of wrong usernames etc. Profiled sshd, showing lots of cpu time in liblzma, with perf unable to attribute it to a symbol. Got suspicious. Recalled that I had seen an odd valgrind complaint in automated testing of postgres, a few weeks earlier, after package updates.</p>
<p>Really required a lot of coincidences.</p>
</blockquote>
<p><cite class="standalone"><a href="https://mastodon.social/@AndresFreundTec/112180083704606941">Andres Freund (a Postgres developer at Microsoft)</a></cite></p>
<p>As noted by <a href="https://news.ycombinator.com/item?id=39879960">nurple</a> on HN, this exploit was found for similar reasons to Ken Thompson's supply chain exploit was detected.  Strange performance sparking curiosity.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://gist.github.com/thesamesam/223949d5a074ebc3dce9ee78baad9e27">FAQ on the exploit</a> by Sam James</li>
<li><a href="https://news.ycombinator.com/item?id=39865810">Initial discussion on hn</a></li>
<li><a href="https://www.cve.org/CVERecord?id=CVE-2024-3094">CVE-2024-3094 Report</a></li>
<li><a href="https://www.openwall.com/lists/oss-security/2024/03/29/4">Original backdoor report by Freund</a></li>
<li><a href="https://bsky.app/profile/did:plc:x2nsupeeo52oznrmplwapppl/post/3kowjkx2njy2b">Discovery of RCE</a></li>
<li><a href="https://lcamtuf.substack.com/p/technologist-vs-spy-the-xz-backdoor">Technologist vs Spy</a>, making the case that based on the behaviour and circumstances surrounding the bug were that of organised actors</li>
<li><a href="https://github.com/Midar/xz-backdoor-documentation/wiki">XZ Backdoor Documentation</a></li>
<li><a href="https://gynvael.coldwind.pl/?lang=en&amp;id=782">XZ Bash-Stage Obfuscation Explained</a></li>
<li><a href="https://arstechnica.com/security/2024/04/what-we-know-about-the-xz-utils-backdoor-that-almost-infected-the-world/"><strong>Ars Technica Overview</strong></a></li>
<li><a href="https://github.com/amlweems/xzbot">PoC</a> with substituted public key</li>
<li><a href="https://shellsharks.com/xz-compromise-link-roundup">Even more links</a></li>
<li><a href="https://research.swtch.com/xz-timeline">Timeline of XZ</a></li>
<li><a href="https://www.arp242.net/jia-tan-go.html">Jia Tanning Go Code</a></li>
</ul>
<p><img alt="" src="https://notes.zachmanson.com/media/xz-analysis.png" /></p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/xz</guid>
            <pubDate>2024-03-31</pubDate>
        </item>
        
        <item>
            <title>Nextcloud Phone Photo Upload</title>
            <link>https://notes.zachmanson.com/nextcloud-phone-photo-upload</link>
            
            <description>Just use FolderSync.</description>
            
            <content:encoded>
                <![CDATA[<p>Nextcloud is excellent, and its Android app is great.  I found the app's Auto-Upload feature to be nigh unusable.</p>
<p>I have 20GiB+ of photos on my phone and would like a backup.  My primary Nextcloud account is synced to my PCs, so any file added to it will be downloaded to all devices.  I don't need local access to these photos on all my PCs, so I created a seperate Nextcloud account just for camera roll uploads. If I needed to access these photos from a PC I could log in to the Nextcloud web interface with the camera roll account.</p>
<p>Attempting to use Auto-Upload on my phone's Camera folder was messy.  It kept uploading photos to the wrong Nextcloud account, incorrectly counting how many files there were, and did not supporting bidirectional sync.  It was unpredictable how long it would take to actually trigger an upload, not having a manual way of starting one.  It also limited automatic uploads to a single folder. These issues were all seperate from the fact that the UI was extremely glitchy as it tried to show 20GiB of photos.</p>
<p>I found using <a href="https://www.tacit.dk/foldersync">FolderSync</a> a much better experience.  It let me set up bidirectional sync between custom Nextcloud folders and local folders, let me choose cadence for syncing (with many configuration options like only allowing upload when charging) and allowed me to manually trigger a sync.  The premium version is absolutely worth it.  I'm only scratching the surface of what it can do here, but for my needs it's perfect.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/nextcloud-phone-photo-upload</guid>
            <pubDate>2024-05-01</pubDate>
        </item>
        
        <item>
            <title>Planting a Flag in Wikipedia</title>
            <link>https://notes.zachmanson.com/planting-a-flag-in-wikipedia</link>
            
            <description>LibKey Nomad's Stolen Valour</description>
            
            <content:encoded>
                <![CDATA[<p>There is a wonderful browser extension with an awful name made by Third Iron called <a href="https://thirdiron.com/products/libkey-nomad/">LibKey Nomad</a>. It allows university staff and students gain access to academic journals that your univesity is subscribed to without logging in or requesting access. You install the extension, sign in with your university credentials, and then it just works. Whenever you are on a site that LibKey Nomad is automatically allowing you access to, it displays this little banner in the corner of the page.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/proper-popup.png" /></p>
<p>You click it and you are taken direct to the paper. It really streamlines the process for a lot of sites.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/wikipedia-popup.png" /></p>
<p>It hilariously appears on Wikipedia articles, as if the University of Western Australia has been incredibly gracious by allowing me to see Wikipedia. The reasoning here is that access to (at least one of) the citations is being provided through LibKey Nomad, but this makes the banner no less obnoxious.</p>
<p>The banner doesn't even have a function like it does on other journals, clicking it just takes you to the references list.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/citations.png" /></p>
<p>Wikipedia is one of the most impressive things humans have created. Keep your banners to yourself, Third Iron.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/planting-a-flag-in-wikipedia</guid>
            <pubDate>2023-02-06</pubDate>
        </item>
        
        <item>
            <title>Code Opinions</title>
            <link>https://notes.zachmanson.com/code-opinions</link>
            
            <description>Zach's Strong Code Opinions (fight me)</description>
            
            <content:encoded>
                <![CDATA[<blockquote>
<p>Break any of these rules sooner than say anything outright barbarous.</p>
</blockquote>
<p><cite class="standalone">George Orwell</cite></p>
<p>This whole website is a living document, but this page is especially living. These are strong beliefs that are weakly held, I'm certain many of these will change over time.</p>
<p>Some of these will seem trivial and obvious, but they are all written because I have seem them consistently violated in professional settings. These writing were initially intended to be a code practices guide, but they became just angry venting.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/code-opinions</guid>
            <pubDate>2025-10-14</pubDate>
        </item>
        
        <item>
            <title>Grep Timer</title>
            <link>https://notes.zachmanson.com/grep-timer</link>
            
            <content:encoded>
                <![CDATA[<p>Grep can be used as a timer, and can be blocking.</p>
<p>This will block until the logs print accept connections twice, or 500 seconds elapses, with an return code to match. Good for blocking a CD pipeline until a certain log has been output.</p>
<div class="highlight"><pre><span></span><code>( docker compose logs postgres -f &amp; ) | timeout 500 grep -m2 &quot;accept connections&quot;
</code></pre></div>
<p>I came across this when using initdb.d with <a href="https://notes.zachmanson.com/docker">Docker</a> and <a href="https://notes.zachmanson.com/postgres">Postgres</a>. initdb.d will run init scripts when the Postgres container is started, but these are internal to the container, so do not block <code>docker start</code> or <code>docker compose up</code>.  Despite this, Postgres cannot be connected to until the init scripts have completed. A quirk of the way the Postgres process in the container runs is that it starts up, loads the script, then restarts. This prints "postgres is ready to accept connections" twice, but only the second time indicates that Postgres is actually ready. This little grep timer was a simple way to block the CD pipeline.</p>
<p>I adapted it from <a href="https://github.com/docker-library/postgres/issues/146#issuecomment-482393929">this</a>.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/grep-timer</guid>
            <pubDate>2024-11-13</pubDate>
        </item>
        
        <item>
            <title>Poetry Showing Dependencies it Refuses to Install</title>
            <link>https://notes.zachmanson.com/poetry-showing-dependencies-it-refuses-to-install</link>
            
            <content:encoded>
                <![CDATA[<p>When Poetry lists the dependencies for a package it doesn't show the Python versions that those ranges are valid for.  For example, <a href="https://github.com/boto/botocore/blob/develop/setup.py">this</a> <code>setup.py</code>:</p>
<div class="highlight"><pre><span></span><code><span class="n">requires</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s1">&#39;jmespath&gt;=0.7.1,&lt;2.0.0&#39;</span><span class="p">,</span>
    <span class="s1">&#39;python-dateutil&gt;=2.1,&lt;3.0.0&#39;</span><span class="p">,</span>
    <span class="c1"># Prior to Python 3.10, Python doesn&#39;t require openssl 1.1.1</span>
    <span class="c1"># but urllib3 2.0+ does. This means all botocore users will be</span>
    <span class="c1"># broken by default on Amazon Linux 2 and AWS Lambda without this pin.</span>
    <span class="s1">&#39;urllib3&gt;=1.25.4,&lt;1.27 ; python_version &lt; &quot;3.10&quot;&#39;</span><span class="p">,</span>
    <span class="s1">&#39;urllib3&gt;=1.25.4,!=2.2.0,&lt;3 ; python_version &gt;= &quot;3.10&quot;&#39;</span><span class="p">,</span>
<span class="p">]</span>
</code></pre></div>
<p>will not have the Python version shown when running <code>poetry show</code>, despite some the package ranges depending on the Python version of the project.  On a Python project where the <code>pyproject.toml</code> specifies Python 3.9, <code>poetry show</code> will only return the urllib3 version range that is valid for Python 3.9.</p>
<div class="highlight"><pre><span></span><code><span class="o">[</span>~/projects/prosebit<span class="o">]</span><span class="w"> </span><span class="o">(</span>develop<span class="o">)</span><span class="w">  </span>
&gt;<span class="w"> </span>head<span class="w"> </span>pyproject.toml
<span class="o">[</span>tool.poetry<span class="o">]</span>
<span class="nv">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;prosebit&quot;</span>
<span class="nv">version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;0.1.0&quot;</span>
<span class="nv">description</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;&quot;</span>
<span class="nv">authors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">[</span><span class="s2">&quot;Your Name &lt;you@example.com&gt;&quot;</span><span class="o">]</span>
<span class="nv">readme</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;README.md&quot;</span>

<span class="o">[</span>tool.poetry.dependencies<span class="o">]</span>
<span class="nv">python</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;3.9&quot;</span>

<span class="o">[</span>~/projects/prosebit<span class="o">]</span><span class="w"> </span><span class="o">(</span>develop<span class="o">)</span><span class="w">  </span>
&gt;<span class="w"> </span>poetry<span class="w"> </span>show<span class="w"> </span>botocore
<span class="w"> </span>name<span class="w">         </span>:<span class="w"> </span>botocore<span class="w">                               </span>
<span class="w"> </span>version<span class="w">      </span>:<span class="w"> </span><span class="m">1</span>.34.144<span class="w">                               </span>
<span class="w"> </span>description<span class="w">  </span>:<span class="w"> </span>Low-level,<span class="w"> </span>data-driven<span class="w"> </span>core<span class="w"> </span>of<span class="w"> </span>boto<span class="w"> </span><span class="m">3</span>.<span class="w"> </span>

dependencies
<span class="w"> </span>-<span class="w"> </span>jmespath<span class="w"> </span>&gt;<span class="o">=</span><span class="m">0</span>.7.1,&lt;<span class="m">2</span>.0.0
<span class="w"> </span>-<span class="w"> </span>python-dateutil<span class="w"> </span>&gt;<span class="o">=</span><span class="m">2</span>.1,&lt;<span class="m">3</span>.0.0
<span class="w"> </span>-<span class="w"> </span>urllib3<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.25.4,&lt;<span class="m">1</span>.27

required<span class="w"> </span>by
<span class="w"> </span>-<span class="w"> </span>boto3<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.34.144,&lt;<span class="m">1</span>.35.0
<span class="w"> </span>-<span class="w"> </span>s3transfer<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.33.2,&lt;<span class="m">2</span>.0a.0
</code></pre></div>
<p>On a Python project where the <code>pyproject.toml</code> specifies Python 3.10, <code>poetry show</code> will only return the urllib3 version range that is valid for Python 3.10.</p>
<div class="highlight"><pre><span></span><code><span class="o">[</span>~/projects/prosebit<span class="o">]</span><span class="w"> </span><span class="o">(</span>develop<span class="o">)</span><span class="w">  </span>
&gt;<span class="w"> </span>head<span class="w"> </span>pyproject.toml
<span class="o">[</span>tool.poetry<span class="o">]</span>
<span class="nv">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;prosebit&quot;</span>
<span class="nv">version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;0.1.0&quot;</span>
<span class="nv">description</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;&quot;</span>
<span class="nv">authors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">[</span><span class="s2">&quot;Your Name &lt;you@example.com&gt;&quot;</span><span class="o">]</span>
<span class="nv">readme</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;README.md&quot;</span>

<span class="o">[</span>tool.poetry.dependencies<span class="o">]</span>
<span class="nv">python</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;3.10&quot;</span>

<span class="o">[</span>~/projects/prosebit<span class="o">]</span><span class="w"> </span><span class="o">(</span>develop<span class="o">)</span><span class="w">  </span>
&gt;<span class="w"> </span>poetry<span class="w"> </span>show<span class="w"> </span>botocore
<span class="w"> </span>name<span class="w">         </span>:<span class="w"> </span>botocore<span class="w">                               </span>
<span class="w"> </span>version<span class="w">      </span>:<span class="w"> </span><span class="m">1</span>.34.144<span class="w">                               </span>
<span class="w"> </span>description<span class="w">  </span>:<span class="w"> </span>Low-level,<span class="w"> </span>data-driven<span class="w"> </span>core<span class="w"> </span>of<span class="w"> </span>boto<span class="w"> </span><span class="m">3</span>.<span class="w"> </span>

dependencies
<span class="w"> </span>-<span class="w"> </span>jmespath<span class="w"> </span>&gt;<span class="o">=</span><span class="m">0</span>.7.1,&lt;<span class="m">2</span>.0.0
<span class="w"> </span>-<span class="w"> </span>python-dateutil<span class="w"> </span>&gt;<span class="o">=</span><span class="m">2</span>.1,&lt;<span class="m">3</span>.0.0
<span class="w"> </span>-<span class="w"> </span>urllib3<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.25.4,&lt;<span class="m">2</span>.2.0<span class="w"> </span><span class="o">||</span><span class="w"> </span>&gt;2.2.0,&lt;<span class="m">3</span>

required<span class="w"> </span>by
<span class="w"> </span>-<span class="w"> </span>boto3<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.34.144,&lt;<span class="m">1</span>.35.0
<span class="w"> </span>-<span class="w"> </span>s3transfer<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.33.2,&lt;<span class="m">2</span>.0a.0
</code></pre></div>
<p>On a Python project where a range is specified that covers multiple urllib3 package ranges, <strong><code>poetry show</code> will show both ranges without specifying that the ranges apply to different Python versions</strong>.</p>
<div class="highlight"><pre><span></span><code><span class="o">[</span>~/projects/prosebit<span class="o">]</span><span class="w"> </span><span class="o">(</span>develop<span class="o">)</span><span class="w">  </span>
&gt;<span class="w"> </span>head<span class="w"> </span>pyproject.toml
<span class="o">[</span>tool.poetry<span class="o">]</span>
<span class="nv">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;prosebit&quot;</span>
<span class="nv">version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;0.1.0&quot;</span>
<span class="nv">description</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;&quot;</span>
<span class="nv">authors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">[</span><span class="s2">&quot;Your Name &lt;you@example.com&gt;&quot;</span><span class="o">]</span>
<span class="nv">readme</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;README.md&quot;</span>

<span class="o">[</span>tool.poetry.dependencies<span class="o">]</span>
<span class="nv">python</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;&gt;=3.9,&lt;3.12&quot;</span>

<span class="o">[</span>~/projects/prosebit<span class="o">]</span><span class="w"> </span><span class="o">(</span>develop<span class="o">)</span><span class="w">  </span>
&gt;<span class="w"> </span>poetry<span class="w"> </span>show<span class="w"> </span>botocore
<span class="w"> </span>name<span class="w">         </span>:<span class="w"> </span>botocore<span class="w">                               </span>
<span class="w"> </span>version<span class="w">      </span>:<span class="w"> </span><span class="m">1</span>.34.144<span class="w">                               </span>
<span class="w"> </span>description<span class="w">  </span>:<span class="w"> </span>Low-level,<span class="w"> </span>data-driven<span class="w"> </span>core<span class="w"> </span>of<span class="w"> </span>boto<span class="w"> </span><span class="m">3</span>.<span class="w"> </span>

dependencies
<span class="w"> </span>-<span class="w"> </span>jmespath<span class="w"> </span>&gt;<span class="o">=</span><span class="m">0</span>.7.1,&lt;<span class="m">2</span>.0.0
<span class="w"> </span>-<span class="w"> </span>python-dateutil<span class="w"> </span>&gt;<span class="o">=</span><span class="m">2</span>.1,&lt;<span class="m">3</span>.0.0
<span class="w"> </span>-<span class="w"> </span>urllib3<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.25.4,&lt;<span class="m">1</span>.27
<span class="w"> </span>-<span class="w"> </span>urllib3<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.25.4,&lt;<span class="m">2</span>.2.0<span class="w"> </span><span class="o">||</span><span class="w"> </span>&gt;2.2.0,&lt;<span class="m">3</span>

required<span class="w"> </span>by
<span class="w"> </span>-<span class="w"> </span>boto3<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.34.144,&lt;<span class="m">1</span>.35.0
<span class="w"> </span>-<span class="w"> </span>s3transfer<span class="w"> </span>&gt;<span class="o">=</span><span class="m">1</span>.33.2,&lt;<span class="m">2</span>.0a.0
</code></pre></div>
<p>Despite displaying both ranges in <code>poetry show</code>, <strong>Poetry will only use the oldest range when running <code>poetry lock</code> or <code>poetry install</code></strong> .  This can lead to a disconnect between the package ranges that Poetry is reporting as valid and the package ranges that Poetry will actually attempt to use when running.</p>
<p>I discovered this the hard way when trying to install <code>kinde-python-sdk</code> and <code>boto3</code> within the same project. <code>boto3</code> requires <code>botocore</code> which requires <code>urllib3</code>.  <code>kinde-python-sdk</code> also requires <code>urllib3</code>. Since my project was set to use <code>python = "&gt;=3.9,&lt;3.12"</code>, Poetry was listing both ranges in <code>poetry show</code>, but only using the range <code>urllib3 &gt;=1.25.4,&lt;1.27</code> when installing <code>botocore</code>. This resulted in headaches because <code>kinde-python-sdk</code> requires <code>urllib3 &gt;=2.2.1,&lt;2.3.0</code>, so it appeared like <code>botocore</code> and <code>kinde-python-sdk</code> could coexist when I ran <code>poetry show</code>, but failed to install every time I tried.</p>
<p>Another important detail to note is that <strong>this is all dependent on the Python version specified in <code>pyproject.toml</code>.  The Python version you are actually running does not change the behaviour of Poetry.</strong>  I ran into all of these problems when running Python 3.11, so was confused for hours.</p>
<p><code>poetry show</code> should report the Python versions that each package dependency range is valid for, since this turned a relatively simple dependency conflict into a multi-hour dependency conflict.  I was only able to figure out the exact cause by reading the setup script for <code>botocore</code>.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/poetry-showing-dependencies-it-refuses-to-install</guid>
            <pubDate>2024-07-15</pubDate>
        </item>
        
        <item>
            <title>Spite Driven Development</title>
            <link>https://notes.zachmanson.com/spite-driven-development</link>
            
            <content:encoded>
                <![CDATA[<p>Spite Driven Development (SDD) is my preferred development methodology.  I have found it by far my most productive style of software engineering, and has been responsible in whole or part for most of my <a href="https://notes.zachmanson.com/projects">projects</a> (see: <a href="https://notes.zachmanson.com/penultimate-guitar">Penultimate Guitar</a>, <a href="https://notes.zachmanson.com/minecraftle">Minecraftle</a>, <a href="https://notes.zachmanson.com/alculator">Alculator</a>, <a href="https://notes.zachmanson.com/status-messenger">Status Messenger</a>).</p>
<p>While it is extremely effective for building software quickly, it is often destructive to other aspects of one's life, such as healthy eating, relationships, and especially sleep.</p>
<h2 id="history">History</h2>
<p>I got mad at a competitor/company/friend with shitty practices/existing tool.  Spend multiple days writing code. <code>git push</code></p>
<h2 id="spite-driven-development-cycle">Spite Driven Development Cycle</h2>
<ol>
<li>get mad</li>
<li>think you can do it better yourself</li>
<li>feel the need to prove you can do better</li>
<li><code>git init</code></li>
<li>design feature with a scope exactly commensurate to your anger</li>
<li>forgo loved ones and human interaction</li>
<li>finish feature</li>
<li><code>git commit</code></li>
<li>see 5</li>
<li>deploy</li>
<li>see 5</li>
<li>run it 40 times just to watch it work, injecting dopamine directly into your veins</li>
<li>rest</li>
</ol>
<h2 id="goals">Goals</h2>
<p>Victory.</p>
<h2 id="non-goals">Non Goals</h2>
<p>Sustenance, testing, clean code.</p>
<h2 id="development-frameworks">Development Frameworks</h2>
<p>Much like how ruby+rspec is very well suited to test driven development, many frameworks are well suited to SDD.  Whichever framework you know is the ideal framework for SDD.  Time is of the essence and you have no-one to please but yourself.  Spite burns hot but it burns dirty, and it is not a renewable resource.  Your must strike while the iron is hot and complete the project before you are sapped of the hatred that started all this.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/spite-driven-development</guid>
            <pubDate>2024-03-18</pubDate>
        </item>
        
        <item>
            <title>Usernames</title>
            <link>https://notes.zachmanson.com/usernames</link>
            
            <description>Why am I still surprised</description>
            
            <content:encoded>
                <![CDATA[<p>I spend an inordinate amount of time reading arguments on the internet. Every now and then I'll find one that is actually thoughtful and in-depth. One with more kindness than usual, with genuine attempts to understand the perspectives of each other.</p>
<p>And after the arguing has concluded, the fighting parties express that they respect the other.</p>
<p>These are rare. But it isn't rare that upon finding one of these I will glance at the usernames.</p>
<p>Today I glanced at the usernames of two people having a beautiful argument on a forum:</p>
<p>"euronforpresident"</p>
<p>And their opponent:</p>
<p>"Hitleresque"</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/usernames</guid>
            <pubDate>2022-03-11</pubDate>
        </item>
        
        <item>
            <title>UWA Computer Science</title>
            <link>https://notes.zachmanson.com/uwa-computer-science</link>
            
            <description>Reviews and advice for all Computer Science, Cyber Security, and Data Science units I completed at UWA.</description>
            
            <content:encoded>
                <![CDATA[<blockquote>
<p>Yea as long as you do all the work and watch all the lectures you’ll be chilling</p>
</blockquote>
<p><cite class="standalone"><a href="https://discord.com/channels/586127025228742656/586575812455563269/783525026573451334">seph</a></cite></p>
<p>I did my <a href="https://notes.zachmanson.com/computer-science">Computer Science</a> degree from 2020 to 2023, coming out with a BSc. Computer Science, Cybersecurity, minoring in Data Science. I have omitted statistics units because I have nothing intelligible to say about them<sup id="fnref:1"><a class="footnote-ref" href="#fn:1">1</a></sup>.</p>
<p>Just do <a href="https://codersforcauses.org/">Coders For Causes</a>, you will learn more in that than entire semesters of this degree. <strong>Learn how to use <a href="https://notes.zachmanson.com/git">Git</a> as fast as possible</strong> then force all your group project members to use it. Narrow down your problems as much as possible, then google the most generic possible description of it. If you see someone using semicolons mid-sentence regularly it's probably <a href="https://notes.zachmanson.com/christ-mcdonald">Chris McDonald</a>.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/uwa-cits.png" /></p>
<h2 id="first-year-units">First Year Units</h2>
<p>CITS1001 Software Engineering with Java (called Object Oriented Programming when I took it in 2020):</p>
<ul>
<li>serves as the introduction to programming for many students</li>
<li>actively bad introduction to programming</li>
<li>spends time describing the conceptual model of objects and encapsulation before describing variables and if statements</li>
<li>class only uses BlueJ (yuck) and as such never teaches you to use a main function. Met students who had completed the class and were still incapable of actually running a Java program</li>
<li>glad that it is being replaced with CITS2005</li>
</ul>
<p>CITS1003 Intro to Cybersecurity:</p>
<ul>
<li>extremely fun introduction to basic cybersecurity concepts</li>
<li>main project was a semester long CTF</li>
<li>perhaps a bit light on content but gives a good taste of the major</li>
</ul>
<p>CITS1401 Computational Thinking with Python:</p>
<ul>
<li>much better intro to programming</li>
<li>weirdly speedruns 2 years of Computer Science ATAR in the first lesson for no reason</li>
<li>took 3 weeks to get to if statements in Python which is pretty poor</li>
<li>decent intro though</li>
</ul>
<p>CITS1402 Relational <a href="https://notes.zachmanson.com/databases">Databases</a></p>
<ul>
<li>dead boring but theres no way not to be, it's databases</li>
</ul>
<h2 id="second-year-units">Second Year Units</h2>
<p>CITS2002 Systems Programming</p>
<ul>
<li>Chris McDonald is an excellent lecturer</li>
<li>good introduction to <a href="https://notes.zachmanson.com/c">C</a></li>
<li>challenging projects but you learn a lot</li>
<li>very rewarding</li>
<li>this and CITS2200 have the highest kill rate for comsci degrees <sup>[citation needed]</sup>. partially because they are hard, partially because the previous units did a pretty bad job preparing students for them</li>
</ul>
<p><a href="https://notes.zachmanson.com/cits2003">CITS2003</a> Open Source Tools &amp; Scripting</p>
<ul>
<li>teaches <a href="https://notes.zachmanson.com/bash">Bash</a> and <a href="https://notes.zachmanson.com/unix">Unix</a> which are important and only orthogonally explained in CITS2002</li>
<li>teaches <a href="https://notes.zachmanson.com/sed">sed</a> and <a href="https://notes.zachmanson.com/awk">AWK</a> relatively well</li>
<li>includes <a href="https://notes.zachmanson.com/make">Make</a> for some reason, no reason literally cut this its covered in CITS2002 and is actually relevant there</li>
<li>also includes <a href="https://notes.zachmanson.com/git">Git</a> but never actually requires its use so it feels useless even though it is most students first introduction to it. git should be in CITS1401.</li>
<li>somehow manages to completely screw up the assessments every year</li>
<li>see <a href="https://notes.zachmanson.com/michael-wise">Michael Wise</a></li>
</ul>
<p>CITS2200 Data Structures &amp; Algorithms</p>
<ul>
<li><a href="https://notes.zachmanson.com/amitava-datta">necessary evil</a></li>
<li>teaches important concepts effectively, relatively dry though</li>
<li>assessments are done in Java and this leads to many stupid problems</li>
</ul>
<p>CITS2211 Discrete Structures</p>
<ul>
<li>the theoretical basis of what a computer actually is</li>
<li>very interesting but also very hard</li>
<li>very disconnected from the rest of the degree sadly</li>
</ul>
<p>CITS2402 Introduction to Data Science</p>
<ul>
<li>actual waste of time</li>
<li>you can learn this entire unit in a day but watching some tutorials on matplotlib, numpy and pandas</li>
<li>feels like watching a fireship video at 0.005 speed</li>
</ul>
<h2 id="third-year-units">Third Year Units</h2>
<p>CITS3001 Algorithms, <a href="https://notes.zachmanson.com/agents">Agents</a> &amp; Artificial Intelligence</p>
<ul>
<li>so much wasted potential</li>
<li>could be one of the most interesting units in the whole degree, seems like it was before my time</li>
<li>tries to cover so much but ends up covering very little effectively</li>
<li>main project was a disaster in 2022</li>
</ul>
<p>CITS3002 Computer Networks</p>
<ul>
<li>Taught by Oppy himself</li>
<li>Very interesting content, well taught</li>
<li>projects get your hands dirty with creating actual protocols and working with sockets</li>
<li>few things were more satisfying in the entire degree than actually running my final project for this over the internet between me and my partners houses and watching it run</li>
<li>spiritual sequel to Systems Programming</li>
</ul>
<p>CITS3003 Graphics and Animation<sup id="fnref:2"><a class="footnote-ref" href="#fn:2">2</a></sup></p>
<ul>
<li>abandoned</li>
<li>feels like it was really interesting in 2009, but the unit coordinator who cared about it has since left and now it wanders the optional units lists like a zombie</li>
<li>project hasn't changed majorly in 10 years, though it has gotten more convoluted</li>
<li>there are certainly past versions of it on github if you are so inclined</li>
<li>soulless, out of date, and wasted potential as it could be one of the most interesting units</li>
</ul>
<p>CITS3006 Penetration Testing</p>
<ul>
<li>amazing</li>
<li>final assessment is literally break into this VM, then secure it and swap with other groups</li>
<li>good balance of pratical and theoretical</li>
<li>Jin Hong is fantastic</li>
<li>lab instructors are all great</li>
<li>had one lecture that wasn't allowed to be recorded because it taught actual live vulnerabilities in UWA systems, excellent, all units should have one of these</li>
<li>best unit in the whole course</li>
</ul>
<p>CITS3007 Secure Coding</p>
<ul>
<li>the worst C unit by default, only because the others are just so good</li>
<li>teaches you how to write vulnerable C code, and how to turn off compiler safety checks</li>
<li>is geniunely interesting</li>
<li>Arran Stuart is great, seems like the lecturer who is most in touch with modern software engineering?</li>
</ul>
<p>CITS3401 Data Warehousing</p>
<ul>
<li>actual trash</li>
<li>feels like I learnt 5 things about large scale data storage and nothing else</li>
<li>project is an arbitrary mess</li>
<li>pratical side of the unit is a joke</li>
<li>got really interesting for 2 weeks when it completely switches gears to talk about graph databases but then lets you down again</li>
</ul>
<p>CITS3403 Agile <a href="https://notes.zachmanson.com/web-development">Web Development</a></p>
<ul>
<li>really fun and very practical</li>
<li>FIRST ACTUAL USE OF <a href="https://notes.zachmanson.com/git">Git</a> (do no wait till third year, learn this in year 1 semester 1) how did it take this long</li>
<li>well taught</li>
<li><a href="https://minecraftle.zachmanson.com">very</a> <a href="https://notes.zachmanson.com/minecraftle">fun project</a></li>
</ul>
<p>CITS3200 Professional Computing</p>
<ul>
<li>your capstone project with an actual client, aka gambling your marks</li>
<li>if your client is chill you will do well, if not you are ruined</li>
<li>e.g. my team shipped actual malware and got a HD</li>
<li>teams are allocated based on having an even WAM spread so get hyped for mark rubber-banding</li>
<li>there are lectures</li>
<li>2/10 lectures will be valuable</li>
<li>3/10 will be companies hyping themselves up</li>
<li>4/10 will be inane ethics bullshit</li>
<li>1/10 will be alleged war criminals</li>
<li>all prior units have not adequately prepared you for this, go learn django or <a href="https://notes.zachmanson.com/react">React</a> because you are very likely going to need to know these. if you are wondering if you should make a web app you should probably just make a web app</li>
</ul>
<h2 id="miscellaneous-advice">Miscellaneous Advice</h2>
<p>Labs are the only thing in the degree you can't get from youtube or the <a href="https://notes.zachmanson.com/internet">Internet</a>. If you are bad, actually do the labs, go in and ask the instructors questions. It's annoying but they can help you bridge the gap between trash and passing.</p>
<p>Learn how to use the command line ASAP. Learn git. Seriously go learn git. So many people I met couldn't use git in their third year. How they made it that far sending code snippets over discord is beyond me. Bully everyone you know into using git. You will actually lose 20% mark if you don't use git+github for a group project because you will waste so much time trying to coordinate the project. If git is hurting your brain try using GitHub Desktop which makes it a bit easier to use for newcomers.</p>
<p>Have other hobbies. Doesn't matter what they are but gaming doesn't count if it's your only hobby.</p>
<p>Go to social events with clubs. The specific club doesn't matter, they are all fine. Alcohol helps, if you are so inclined (science union pub crawls are great). It's very easy in computer science to not make actually make any friends and do everything online. But you will need friends to not be sad and die alone.</p>
<p>If you've made it this far, here's <a href="https://guzey.com/college">another opinionated essay on university</a>.</p>
<div class="footnote">
<hr />
<ol>
<li id="fn:1">
<p>When I started my degree I was enrolled in Computer Science and Data Science as my majors. Data Science units come in two forms at UWA, the computer science ones and the statistics ones. The computer science ones were a somewhat random grab-bag of CITS units, all in Python.  They tended to be relatively easy coding and not very deep.  The STAT units, to me, were HARD. The code was all in R, which is a language made by mathematicians not computer scientists and it shows. You can use it to do some very powerful data manipulation very easily, it has incredible libraries for data processing and visualisation, but holy shit writing it breaks my brain and all I did was remember the 8 functions we had been taught in labs and remember which params needed to be switched out. Every two weeks there would be a "lab report" which was a worksheet based on the lessons where you do some data processing in R.  These would <strong>always</strong> take me a full week to complete, which mostly consisted of trial and erroring the functions we had been taught until I had some data that kinda looked like it could mean something if you squinted just right (just like actual data scientists I suppose).  I felt like the classes were taught poorly by angry men. The time spent on the foundational concepts never made sense to me, and did not make sense to any of the other students in my class that I spoke to.  Inexplicably, I did well in pretty much all of the STAT units despite never understanding what I was doing or the underlying mathematical concepts. <strong>Being able to do well while understanding nothing, to me, is the biggest indicator a class is worthless.</strong>  I came to feel that these classes only provided me stress and drew time away from other classes that I actually cared about. At least they were cheap.&#160;<a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text">&#8617;</a></p>
</li>
<li id="fn:2">
<p>This unit is so decrepit that as of 2024-08-09 the <a href="https://teaching.csse.uwa.edu.au/units/CITS3003/">course website</a> doesn't even have CSS anymore&#160;<a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text">&#8617;</a></p>
</li>
</ol>
</div>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/uwa-computer-science</guid>
            <pubDate>2023-06-19</pubDate>
        </item>
        
        <item>
            <title>snakeware</title>
            <link>https://notes.zachmanson.com/snakeware</link>
            
            <description>A Linux-based operating system with a pure Python userspace.</description>
            
            <content:encoded>
                <![CDATA[<p><img alt="" src="https://notes.zachmanson.com/media/snakeware.png" /></p>
<p>snakeware is an experimental <a href="https://notes.zachmanson.com/linux">Linux</a>-based OS with developed in 2020.  Its userspace is written entirely in Python, barring a single Busybox root process.  It was created by Joshua Moore drawing inspiration from the Commodore 64.  The window manager, snakewm, is written primarily with pygame_gui.  It is not based on X11 or Wayland, instead writing directly to the framebuffer (<code>/dev/fb0</code>).</p>
<p>On first boot, snakeware launches you directly a the Python interpreter.  From here you can run Python commands directly, launch the window manager, or launch CLI games.</p>
<p>To launch snakewm from the interpreter, run the <code>snakewm</code> command.  From snakewm the aopp menu can be opened using the Super key.  From here a number of applications can be launched.</p>
<p>To launch CLI games, enter the following commands:</p>
<div class="highlight"><pre><span></span><code>&gt;&gt;&gt; import snake_games
&gt;&gt;&gt; snake_games.menu()
</code></pre></div>
<p>Games included as of 2022 are snake, snakepaint, and circlez.</p>
<h2 id="history">History</h2>
<p>snakeware was a short-lived project and should be considered inactive, with most significant contributions ending in 2020.  It was created and maintained by Joshua Moore and had a small number of contributors.  Some of the applications included in snakeware never reached their intended goals, such as the included terminal emulator snaketerm which is extremely rudimentary.</p>
<p>One of the goals in making the entire userspace in Python was to make it modifiable by the user with little effort.  As Python is interpreted, all code for the window manager and the interface would be visible and modifiable by any snakeware user, and would be easy to extend.  Unfortunately, the planned text editor for snakeware, CATH, was never finished.</p>
<p>Other planned projects were networking, a chat application, and a package system built on top of PyPI called snakeoil.</p>
<p>The project was coordinated over a Discord server, though this hasn't been active for some time.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://github.com/joshiemoore/snakeware">snakeware source code</a></li>
<li><a href="https://github.com/Cyppa/CATH-for-Snakeware">CATH text editor</a></li>
</ul>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/snakeware</guid>
            <pubDate>2024-03-26</pubDate>
        </item>
        
        <item>
            <title>GitHub Pages Redirection</title>
            <link>https://notes.zachmanson.com/github-pages-redirection</link>
            
            <description>This feed has moved and GitHub Pages won't let me redirect you automatically.</description>
            
            <content:encoded>
                <![CDATA[<p>(This was written as to tell people to move from my old RSS feed to my new RSS feed on notes.zachmanson.com.  If you are reading this on notes.zachmanson.com you are in the right place.) </p>
<p>I haven't written on here in a while, but I have been writing on my <a href="https://notes.zachmanson.com/">public notes</a>. At this point I've got over 300 pages on there, far more than I ever wrote on here but in a format that looks a lot more like a wiki. This is mostly a factor of convenience, the format of the <a href="https://notes.zachmanson.com/notes">notes</a> means I can iteratively add whatever I am thinking about, without the expectation of the writing being complete and canonical. The writing is organised into a hierarchy of folders, with <a href="https://notes.zachmanson.com/tags">tags</a> and backlinks. I find this format much better for wiki-style writing but doesn't lend itself well to chronological feeds like this one.</p>
<p>Occasionally I would write a note on there that would fit well into the blog format, or was long and more story based. Some of my posts on this blog worked much better as wiki pages than as blog posts. I have wanted to unify these two sites for a long time now. Last week I finally got around to it!</p>
<p>The <a href="https://notes.zachmanson.com/ochrs">Ochrs</a> site generator that I wrote to build the wiki can now generate chronological feeds of posts and RSS feeds. Feeds are generated based on tags, where each tag has its own <a href="https://notes.zachmanson.com/rss">RSS</a> feed. A note can be added to a chronological feed by adding a publish date (<code>date</code> field in the frontmatter), which will then be used as the publish date in all feeds that the note is a part of. This approach is great since it allows each note to exist anywhere in the wiki hierarchy while also being a part of many feeds. The RSS feed will be at <code>notes.zachmanson.com/&lt;tagname&gt;.xml</code>, and the list of notes in a chronological feed can be shown on a page using the <code>ochrs:chrono:&lt;tagname&gt;</code> build function (see <a href="https://notes.zachmanson.com/ochrs-syntax">Ochrs Syntax</a> for more on how this works), which will results in all the posts being displayed in blog-style <a href="https://notes.zachmanson.com/html">HTML</a>.</p>
<p>To succeed <code>zachmanson.com/blog/</code> I have created the posts tag. The feeds can be seen at <a href="https://notes.zachmanson.com/posts">notes.zachmanson.com/posts</a> and using <a href="https://notes.zachmanson.com/posts.xml">RSS</a>. I have moved the existing posts here over to the this new feed.</p>
<p>If you are reading this using RSS then I would like to apologise that you will have to subscribe to a new feed. I would have liked to automatically redirect your feed reader, but <strong>GitHub Pages doesn't let you create 301 redirects</strong>. The <code>redirect-from</code> plugin is capable of meta tag redirects, but those are soft and not followed by feed readers. This is very frustrating.</p>
<p>I've marked a bunch of longer notes I've made as posts, so the new feed already has 7 new posts. Hopefully more will come soon!</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/github-pages-redirection</guid>
            <pubDate>2024-03-29</pubDate>
        </item>
        
        <item>
            <title>Git Switchblade</title>
            <link>https://notes.zachmanson.com/git-switchblade</link>
            
            <content:encoded>
                <![CDATA[<p>A script that will attempt to stash changes on your current branch when you switch(blade), and will restore those changes when you switch(blade) back.</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/usr/bin/env bash</span>


<span class="nv">TARGET</span><span class="o">=</span><span class="nv">$1</span>

<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$TARGET</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">  </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Error: No target branch specified.&quot;</span><span class="w"> </span>&gt;<span class="p">&amp;</span><span class="m">2</span>
<span class="w">  </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Usage: git switchblade &lt;branch-name&gt;&quot;</span><span class="w"> </span>&gt;<span class="p">&amp;</span><span class="m">2</span>
<span class="w">  </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span>
<span class="k">fi</span>

<span class="nv">CURR</span><span class="o">=</span><span class="k">$(</span>git<span class="w"> </span>rev-parse<span class="w"> </span>--abbrev-ref<span class="w"> </span>HEAD<span class="k">)</span>
git<span class="w"> </span>stash<span class="w"> </span>push<span class="w"> </span>-u<span class="w"> </span>-m<span class="w"> </span><span class="s2">&quot;switchblade on </span><span class="nv">$CURR</span><span class="s2">&quot;</span>
git<span class="w"> </span>switch<span class="w"> </span><span class="nv">$TARGET</span>

<span class="nv">STASH_ID</span><span class="o">=</span><span class="k">$(</span>git<span class="w"> </span>stash<span class="w"> </span>list<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s2">&quot;switchblade on </span><span class="nv">$TARGET</span><span class="s2">&quot;</span><span class="p">|</span><span class="w"> </span>head<span class="w"> </span>-n<span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>cut<span class="w"> </span>-d:<span class="w"> </span>-f1<span class="k">)</span>

<span class="c1"># Check if a stash identifier was found</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$STASH_ID</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">  </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Found matching stash: </span><span class="nv">$STASH_ID</span><span class="s2">. Popping it...&quot;</span>
<span class="w">  </span>git<span class="w"> </span>stash<span class="w"> </span>pop<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$STASH_ID</span><span class="s2">&quot;</span>
<span class="k">else</span>
<span class="w">  </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;No stash found containing the word &#39;switchblade&#39;.&quot;</span>
<span class="k">fi</span>
</code></pre></div>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/git-switchblade</guid>
            <pubDate>2025-10-15</pubDate>
        </item>
        
        <item>
            <title>Its</title>
            <link>https://notes.zachmanson.com/its</link>
            
            <content:encoded>
                <![CDATA[<p><em>It(')s</em> has an interesting history. When I was in school I was taught that there are two forms of the word <em>it(')s</em></p>
<ul>
<li>its - possessive</li>
<li>it's - contraction of <em>it is</em></li>
</ul>
<p>If I were in school and mixed up these two I would be admonished. There's a part of my brain I will never be able to exorcise the belief that confusing these two spellings is a sign of poor grammar. But equally, my brain never liked the apostrophe-less <em>its</em> for possession. The word seemed naked to me.</p>
<p><em>It(')s</em> sits at the crossroads of the two main uses of the apostrophe in English, contraction and possession.</p>
<p>The possessive form of <em>it(')s</em> has an interesting quirk in that it sits at the crossroads of another inconsistency - do possessive pronouns get apostrophes? If you think of <em>it</em> as a pronoun akin to <em>his</em>, <em>hers</em>, and <em>theirs</em> (sometimes called "definite" pronouns) then the apostrophe-less <em>its</em> seems fine. If you think of <em>it</em> as similar to other neutral pronoun possessive like <em>one's</em> or <em>somebody's</em> ("indefinite" pronouns, then the absent apostrophe seems inconsistent. The concept of definite pronouns was never pointed out to me in school, primary school me intuited that <em>its</em> should be treated the same as my name but I was told that it was just an edge case I would have remember forever</p>
<p>My childhood assumption is more reasonable than my teachers would have told me.  Like many grammar rules, the rules around <em>it(')s</em> have changed over time. <a href="https://www.etymonline.com/search?q=its">Etymonline</a> claims possessive <em>it(')s</em> dates back to the 1500s and was originally spelt <em>it's</em>, only in the 1800s did the apostrophe fall out of use. It seems like the 1800s change occurred to make room for contracted <em>it's</em> because the other contraction for <em>it is</em>, <em>'tis</em>, fell out of fashion.</p>
<p>Given the attitude my teacher had when teaching this "rule", I find the youth of this "rule" very satisfying. The unconvincing arguments they gave supporting it are not set in stone.</p>
<p>The part that is most stupid about this whole situation to me is that the apostrophe does very little to actually make things clearer. It is extremely hard to construct sentence where the meaning of <em>it(')s</em> cannot easily be gleaned from context, and most sentences where either form is valid in the same place sound awkward.</p>
<p>The closest example I have found to a sentence where the apostrophe adds semantic clarity is this:</p>
<blockquote>
<p>The company announced it(')s plans for expansion.</p>
</blockquote>
<p>If <em>it(')s</em> is possessive, then semantically it means "the company has announced the plans it has for expansion".</p>
<p>If <em>it(')s</em> is a contraction, then semantically the sentence means "the company has announced it has plans for expansion".</p>
<p>These are different, but I find the second form very awkward.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/its</guid>
            <pubDate>2026-03-24</pubDate>
        </item>
        
        <item>
            <title>Kinde Design Decisions</title>
            <link>https://notes.zachmanson.com/kinde-design-decisions</link>
            
            <content:encoded>
                <![CDATA[<p>This is a list of problems with the Kinde interface.</p>
<h2 id="useless-dropdowns">Useless Dropdowns</h2>
<p>This dropdown should list the environments instead of revealing a button to take you to a page that lists the environments. This is an unnecessary second click.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/kinde-1.png" /></p>
<p>This also means you are unable to switch environment without losing the page you are on.</p>
<h2 id="unclickable-icons">Unclickable Icons</h2>
<p>For some reason the left facing chevron is not clickable, only the word "Home" is.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/kinde-2.png" /></p>
<h2 id="bizarre-breadcrumbs">Bizarre Breadcrumbs</h2>
<p>When you go to the "Users Profile" screen, the top left back icon doesn't take you back to the "Users" screen, but remains a "Home" button.</p>
<p>The only way to navigate back is the the small grey "User" text above the user's name.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/kinde-3.png" /></p>
<p>This is something that almost all basic websites can get right.  Breadcrumb navigation or consistent back behaviour would be vastly preferable here.</p>
<p>There is little consistency between parts of the interface that are clickable and those that are not.  This makes navigating quite difficult.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/kinde-design-decisions</guid>
            <pubDate>2024-07-01</pubDate>
        </item>
        
        <item>
            <title>Web Development</title>
            <link>https://notes.zachmanson.com/web-development</link>
            
            <content:encoded>
                <![CDATA[<blockquote>
<p><strong>"Full-stack" now includes a lot more pancakes.</strong> We've added layers and layers of abstractions, tools, and professionalism into every stage of "hosting a site." This is in part due to real factors related to adding hundreds of millions of people to the internet, giving them always-on supercomputers they check at several dozen times a day, common infrastructure and cloud platforms providing solutions for minutiae that used to gate out amateur players, and the explosion of VC/zero-interest capital in the last decade meaning a ton of companies were playing the "eat the world or die" game.</p>
</blockquote>
<p><cite class="standalone"><a href="https://morepablo.com/2022/11/programming-culture-in-the-late-aughts.html">Pablo Meier</a></cite></p>
<p>Web development is an exercise in distributed execution of program, where some parts of your program execute in a (mostly) trusted environment on the server and some execute on a untrusted environment on the client.  Almost all problems in web development can be derived from this breakdown, and the limitations of each of these environments.</p>
<ol>
<li>How can these two environments communicate efficiently, securely, and effectively</li>
<li>Which processing is better handled on the server and which is better handled on the client</li>
<li>Half of your program runs in an untrusted environment</li>
<li>Half of your program has a limited set of system resources that are shared between all users, performance of the server can impact all users simultaneously</li>
</ol>
<p>None of this has even gotten to <a href="https://notes.zachmanson.com/javascript">JavaScript</a> and <a href="https://notes.zachmanson.com/css">CSS</a>, each of which are their own rabbit-holes.</p>
<p>Problems that arise from this:</p>
<ul>
<li>You have a machine in your house you want to serve <a href="https://notes.zachmanson.com/html">HTML</a> files to people on the internet (Apache, Nginx)<ul>
<li>They don't look pretty (CSS)<ul>
<li>Your CSS looks bad on small screens! (responsive design, flexbox)</li>
</ul>
</li>
<li>There's no way to make them change without manually changing the HTML (server rendering, initially with Perl, PHP)<ul>
<li>You want your users to be able to send information to you (HTML Forms and later APIs)<ul>
<li>You want certain users to see different things (<a href="https://notes.zachmanson.com/authentication">Authentication</a> and <a href="https://notes.zachmanson.com/authorization">Authorization</a>)</li>
</ul>
</li>
<li>You want to store data used to render your site, and store data about your users (<a href="https://notes.zachmanson.com/databases">databases</a>, MySQL, <a href="https://notes.zachmanson.com/postgres">Postgres</a>)<ul>
<li>You need to ensure that your users can't sneak database queries into their data (<a href="https://notes.zachmanson.com/sql">SQL</a> sanitisation)</li>
<li>Your programming <a href="https://notes.zachmanson.com/language">language</a> of choice doesn't match the model that <a href="https://notes.zachmanson.com/sql">SQL</a> databases use (<a href="https://notes.zachmanson.com/orms">ORMs</a>)</li>
<li>Your database is too slow! Your schema wasn't designed for efficient queries with so many users. You need to redesign the database and migrate the data to the new schema (database schema migrations and revision management)</li>
<li>Still too many people are trying to read and write to your database at once (Redis, write-through caching)</li>
<li>Your database is getting too slow! You need to start scaling (Vertical scaling, data migration)</li>
<li>Even on the biggest server you can afford you database is still too slow! (sharding, horizontal scaling)</li>
</ul>
</li>
</ul>
</li>
<li>There's no way to make that page move (<a href="https://notes.zachmanson.com/javascript">JavaScript</a>)<ul>
<li>The page is programmable but there's no way to make that page request more data (XMLHttpRequest)<ul>
<li>Sending XML kinda sucks (JSON)</li>
<li>Now that you can send between your site and your server using APIs! But you need to ensure that the requests and responses actually contain the data you expect them to (Request and Response validation, JSON de/serialisation)</li>
<li>You only want your own website to be able to send your server requests (CORS)<ul>
<li>You want other websites to send your server requests (CORS)</li>
</ul>
</li>
</ul>
</li>
<li>JavaScript is really annoying and varies between browsers (JQuery)<ul>
<li>some new JavaScript features are really nice but old browsers don't support them (polyfills, Babel, Lebab)</li>
</ul>
</li>
<li>Even with JQuery, complex applications become pretty hard to manage (JavaScript libraries for complex web applications, AngularJS, <a href="https://notes.zachmanson.com/react">React</a>, many many more)<ul>
<li>These libraries are so nice we want to use them for everything instead of gross HTML (JS managed routing, React Router, single page applications)</li>
<li>Interface design with CSS kinda sucks for especially for complex applications (SASS, Tailwind, prebuilt UI component libraries, Styled Components)</li>
<li>Complex applications need complex application state models (Redux, Zustand, NGXS)</li>
<li>Complex applications need sane data fetching patterns and caching (React Query, SWR, <a href="https://notes.zachmanson.com/rtk-query-vs.-infinite-scrolling">RTK Query</a>)<ul>
<li>It's annoying to have to build bespoke APIs for every different combination of data that your frontend wants, wouldn't it be nice if the frontend could just tell us what it wanted and the backend figured it out (GraphQL)</li>
</ul>
</li>
<li>Even with caching and internal state and JS routing, single applications are pretty slow to actually show useful information since they take multiple requests roundtrips to build the UI and then load the data<ul>
<li>Server rendered frontend frameworks, where the frontend code for a page is run on the server before sending it to the client so that the initial data is already present on first load, then normal SPA behaviour can take over after that (made possible because of Node.js, <a href="https://notes.zachmanson.com/next.js">Next.js</a>, Remix)<ul>
<li>Your server now needs to run JavaScript, which is probably only possible using Node.js</li>
<li>This is great for basic applications but starts to get awkward when you have complex data structures and component structures as you need to know all data requirements at a route level rather than a component level<ul>
<li>Server components allow server side rendering on a component level, by running some of the rendering code on the backend, populating it with data, and slotting that wholesale into a SPA (React Server Components)</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>This language you've been using JavaScript has constant type errors, if only you could specify types (<a href="https://notes.zachmanson.com/typescript">TypeScript</a>, Flow)</li>
<li>Both JavaScript and TypeScript are bad at actually representing HTML (JSX)</li>
</ul>
</li>
<li>Your marketing team wants a website they can control without writing code (CMS, Wordpress, many more)<ul>
<li>They want it on the same domain name (server routing configuration, e.g. Nginx conf)</li>
</ul>
</li>
<li>Your backend server machine's operating system needs to be configured for security (Linux system administration)<ul>
<li>Uh oh! CVE in one of the libraries your server uses! You need a way to update your dependencies (package managers)</li>
<li>Your server state is getting a bit complicated, you aren't sure if you could recreate it now if you wanted to. You also have trouble matching it on your development device.  (<a href="https://notes.zachmanson.com/docker">Docker</a>)</li>
<li>Your backend server is being crushed by your immense popularity, (bigger server, vertical scaling)</li>
<li>You bought the biggest server you can afford and its still too much traffic! (horizontal scaling)<ul>
<li>How do you distribute load across your multiple servers (load balancing)</li>
<li>Sometimes your load is so high that you need to add more servers to your fleet of servers, but it's annoying to do it manually (Kubernetes)</li>
</ul>
</li>
</ul>
</li>
<li>Managing your own servers is getting time consuming, if only there was a way to have code handle the provisioning of servers (Terraform)</li>
</ul>
</li>
</ul>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://frantic.im/javascript-gom-jabbar/">JavaScript Gom Jabbar</a></li>
<li><a href="https://frankchimero.com/blog/2015/the-webs-grain/">The Web's Grain</a></li>
<li>Frontend<ul>
<li><a href="https://themer.dev/blog/the-single-most-important-factor-that-differentiates-front-end-frameworks">The single most impor­tant factor that dif­fer­enti­ates front-end frame­works</a></li>
</ul>
</li>
<li><a href="https://www.kryogenix.org/code/browser/everyonehasjs.html">Everyone Has JavaScript, Right?</a></li>
<li><a href="https://bower.sh/my-love-letter-to-front-end-web-development">My love letter to front-end web development</a></li>
<li><a href="https://grayduck.mn/2024/11/21/handling-cookies-is-a-minefield/">Handling Cookies is a Minefield</a></li>
<li><a href="https://blog.heroku.com/how-to-make-progressive-web-app">Creating PWAs</a></li>
<li><a href="https://www.stavros.io/posts/why-is-everything-so-scalable/">Why is everything so scalable?</a></li>
<li><a href="https://www.youtube.com/watch?v=xE9W9Ghe4Jk&amp;t=107s">Shipping a button in 2026...</a></li>
</ul>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/web-development</guid>
            <pubDate>2026-01-14</pubDate>
        </item>
        
        <item>
            <title>Copilot Edited an Ad Into My PR</title>
            <link>https://notes.zachmanson.com/copilot-edited-an-ad-into-my-pr</link>
            
            <content:encoded>
                <![CDATA[<p>After a team member summoned Copilot to correct a typo in a PR of mine, <strong>Copilot edited my PR description to include and ad for itself and Raycast</strong>.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/copilot-hell.png" /></p>
<p>This is horrific. I knew this kind of bullshit would happen eventually, but I didn't expect it so soon.</p>
<blockquote>
<p>Here is how platforms die: first, they are good to their users; then they abuse their users to make things better for their business customers; finally, they abuse those business customers to claw back all the value for themselves. Then, they die.</p>
</blockquote>
<p><cite class="standalone"><a href="https://notes.zachmanson.com/tiktoks-enshittification">Cory Doctorow</a></cite></p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/copilot-edited-an-ad-into-my-pr</guid>
            <pubDate>2026-03-30</pubDate>
        </item>
        
        <item>
            <title>Minecraftle v2.0</title>
            <link>https://notes.zachmanson.com/minecraftle-v2.0</link>
            
            <content:encoded>
                <![CDATA[<p><a href="https://notes.zachmanson.com/minecraftle">Minecraftle</a> blew up!  At the time of writing it has had over 1 million players and seems to still be increasing.  Seems like it became popular thanks to a few YouTube videos and TikToks. This huge spike in players revealed how poorly made it all was.</p>
<p>The stats page in particular was poorly done.  Originally I had made the schema as simple as possible and only stored the record of each game in one big table. This worked for small player counts, but meant calculating rankings was quadratic so the stats page eventually 10+ seconds to load.  I only discovered this was happening when the <a href="https://notes.zachmanson.com/databases">database</a> for <a href="https://notes.zachmanson.com/penultimate-guitar">Penultimate Guitar</a> would be killed to allocate more resources every time someone loaded the stats page.  </p>
<p>Since rewriting the database meant I'd need to get rid of the entire backend, I decided this was the time to move away from Flask and static JavaScript and port it to Next.js.  I had been meaning to do this for a while.  The Vanilla JS I had been using prior was very janky and poorly made.  I was still unemployed at this time and had plenty of free time since most of my friends had left Melbourne to see their families for Christmas.</p>
<p>The new version of Minecraftle is deployed now on Vercel with a proper instance of Postgres for storing user scores. Rankings are still slow to calculate since all million+ players must be ordered, but they are calculated once per hour via a cronjob that refreshes a materialised view so the pageload is fast.  I'm sure I could do something clever here, chunking the calculations or having them incrementally calculated when a new game score is submitted, but this works for now.</p>
<p>Alongside this rewrite I was able to add a few long requested features, like a clear button.</p>
<h2 id="mistakes">Mistakes</h2>
<h3 id="database-broke">Database Broke</h3>
<p>I accidentally ran the migration script twice, once a few weeks prior to deployment to generate some test data and once on the day I deployed.  I forgot about the initial run in the intervening weeks.  Making matters worse, the second run used the old dump of the database, meaning that all scores were double what they were 3 weeks prior.  This was painful to diagnose as some scores had gone up while others had gone down.  I was eventually able to figure out what had happened thanks to <a href="https://github.com/zachpmanson/minecraftle/issues/34">this bug report</a> which contained a screenshot of the scores the day before.  Thank you very much <a href="https://github.com/suppergerrie2">suppergerrie2</a>!</p>
<h3 id="cdn-vs-audio-files">CDN vs Audio Files</h3>
<p><img alt="" src="https://notes.zachmanson.com/media/mctl-routes.png" /></p>
<p>Back when Minecraftle was just a uni project, one of my teammates, Harrison Oates, added a button that would play in game audio from Minecraft.  This was cute and insignificant, to the point where I forgot it existed.  When I deployed on Vercel this became a problem since almost all my bandwidth (100GiB) was used by this single mp3 file! I removed this feature, but other static assets were going to use my remaining bandwidth.  I had to switch to running it on a new DigitalOcean droplet for the rest of the month.</p>
<p>Hopefully this are the last updates Minecraftle will need for a long time!</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/minecraftle-v2.0</guid>
            <pubDate>2024-04-08</pubDate>
        </item>
        
        <item>
            <title>Thoughts on Melbourne</title>
            <link>https://notes.zachmanson.com/thoughts-on-melbourne</link>
            
            <content:encoded>
                <![CDATA[<p>There is just so much <em>more</em>.  Every little alleyway seems to have something interesting going on.  The most interesting parts of Perth are echoes of various parts of Melbourne.  What comes off as a gimmick or novelty in Perth is a normal and common occurrence here.  I've accidentally wandered into more art galleries here than Perth even has, to my knowledge.</p>
<p>I miss having more friends than I can handle.  In Perth I would often whine about how I longed for time on my own, but I rarely experience that here.  I have far more time on my own now so I guess that desire is satiated.  Here I have my WA-expat friends and the people they have introduced me to.  A few of those have graduated into actual friends, it's a work in progress.</p>
<p>I'm very grateful that the expats have looked after me, I'd have been lost without them.  They've been carrying me so hard.  Despite that, when they are busy or I fear I'm smothering them, I have nobody to see. That's something I haven't felt in many years, if ever.</p>
<p>When I have an idea for a project that excites me I often think of nothing else.  I'll stay up till dawn working on the project, not letting myself rest until the feature is done or the bug is fixed.  This is complemented by by a desire to always be progressing in one way or another.  This manifests as a constant tug of wanting to learn something, progress a skill.  It was the feeling that drove me to start learning piano (though everything I learnt must be atrophied now).</p>
<p>When in the grips of this, anything unimportant feels like a waste of time.  For example, video games lose their appeal immediately.</p>
<p>This is all preamble to the point: the normal drive I have to build something or create something is still there but my brain has made "making new friends" the primary goal.  Any time free time I'm not spending on this makes me feel wasteful. This is often.</p>
<p>When I can't be progressing socially I'm trying to fill my time with other ancillary goals, improving at sketching, cooking, exploring Melbourne.  This helps, but it doesn't quite scratch the itch.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/thoughts-on-melbourne</guid>
            <pubDate>2023-10-24</pubDate>
        </item>
        
        <item>
            <title>Minecraftle v1.1</title>
            <link>https://notes.zachmanson.com/minecraftle-v1.1</link>
            
            <description>Enumerating some small usability changes</description>
            
            <content:encoded>
                <![CDATA[<p><a href="https://minecraftle.zachmanson.com">Minecraftle</a> is a simple game written for my introductory web development unit (CITS3403 with Tim French, would recommend) in 2022 Semester 1. The project was submitted (and completed) on May 23, but since then I have been periodically updating it with "nice to have" features that we didn't manage to get in before the deadline. After 8 months I am happy to label this <a href="https://notes.zachmanson.com/minecraftle">Minecraftle</a> version 1.1, with changes stemming from three main motivations: usability, optimisation, and revenge.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/git-log.png" /></p>
<h2 id="usability">Usability</h2>
<p><code>537c928</code>: Made the emoji summary more vague, so a correct guess shows up as 9 green squares no matter what, rather than giving away the shape of the final solution.</p>
<details>
<summary>Before and after comparison</summary>
Before:

<div class="highlight"><pre><span></span><code>Minecraftle 24/05/2022

⬜⬜⬜
⬜⬜⬜
⬜⬜⬜

🟩🟩🟨
⬜🟩⬜
⬜🟩⬜

🟩🟩⬜
🟩🟩⬜
⬜🟩⬜
</code></pre></div>

After:

<div class="highlight"><pre><span></span><code>Minecraftle 25/05/2022 3/10
⬜🟩⬜
⬜⬜⬜
⬜⬜⬜

⬜⬜⬜
🟩🟩⬜
🟨⬜⬜

🟩🟩🟩
🟩🟩🟩
🟩🟩🟩
</code></pre></div>

</details>

<p><code>23dc225</code>: Added check so that only valid Minecraft recipes could be submitted as guesses.</p>
<p><code>bc40b98</code>: Added proper support for horizontal reflections of recipes.</p>
<p><code>f3295d7</code> &amp; <code>36386df</code>: Made end-of-game popup display different text for Random games.</p>
<p><code>fef189d</code>: Changed the given items for a more interesting set of possible recipes.</p>
<p><code>30a3829</code>: Made end-of-game popup togglable.</p>
<p><code>f43b062</code>: Added loading indicator to initial pageload.</p>
<p><code>8a842d9</code>: Added slot highlighting to the ingredients panel to help keep track of which items have been used before.</p>
<p><img alt="" src="https://notes.zachmanson.com/media/mc-panel.png" /></p>
<h2 id="optimisation">Optimisation</h2>
<p><code>c502eb2</code>: Rewrote the entire script that turned the 600+ Minecraft crafting recipe JSON files into single file with a usable schema. Previous implementation was very messy, future modifications will be much easier.</p>
<p><code>172b3f2</code>: Optimised recipes validation, still relatively inefficient though.</p>
<p><code>cd0c61e</code>: Made game submission API a seperate endpoint to user stats endpoint, rather than being a hacky overload of the stats endpoint.</p>
<p><code>007865b</code>: Significantly reduced size of <code>recipes.json</code> to only include recipes that are actually possible to create with the given ingredients. This reduces initial pageload time and reduces load on my server.</p>
<p><code>007865b</code>: Set <code>init.js</code> to store <code>recipes.json</code> and <code>items.json</code> in local storage, and only request them if they don't exist there. This makes pageloading much faster, and reduces load on my server.</p>
<h2 id="revenge">Revenge</h2>
<p>Since initial release 2500+ people have played the game. From what I can tell this primary originated from popularity at a local primary school and the now-defunct <a href="https://web.archive.org/web/20220927035303/https://likewordle.com/">LikeWordle.com</a>. Most of these players haven't become regular users though, only playing one or two games. Really its just me and my ex competing for the number one spot on the leaderboard. After months of hard work she overtook me as number 1 and I have been clawing my way back ever since. Obviously I could cheat since I'm the admin but where's the fun in that? Despite that, I'm not opposed to adding features to help me take back the crown.</p>
<p><code>309a557</code>: Added indicators to the stats page to show users how far behind and ahead they are of their neighbours on the leaderboard.</p>
<p><code>5dca9d2</code>: Added an account recovery button since I accidentally deleted my account the day after taking back the crown (I swear). This does expose a prexisting security flaw, but does enable it to be used in a positive way. For reference, clearing your browser data will make your device forget which account is it associated with.</p>
<h2 id="bugfixes">Bugfixes</h2>
<p><code>23dc225</code>: Fixed bug caused by the use of item tags in Minecraft source files. Item tags represent groups of items that are effectively interchangable in crafting recipes, such as coal and charcoal. Minecraftle conversion scripts have been updated to expect this, and will replace these with the preferred variant e.g. coal/charcoal group will be replaced with coal.</p>
<p><code>d98e83a</code>: Forced game to be set to Perth timezone. Deployment VPS is in North America, so the server-side calculation of the current recipes wasn't changing at midnight in Perth, where most of the player base is.</p>
<p><code>c0ce8c2</code>: Fixed stick recipe being overwritten by second stick recipes that uses bamboo.</p>
<hr />
<p>It was some of the first <a href="https://notes.zachmanson.com/javascript">JavaScript</a> I ever wrote and it reflects that, much of the old code makes me want to cringe. With the benefit of 6 months professionally working on large <a href="https://notes.zachmanson.com/typescript">TypeScript</a> projects, Minecraftle seems quaint (and somewhat shocking that it even works).</p>
<p>Regardless, it holds a place in my heart as my first <a href="https://notes.zachmanson.com/web-development">web development</a> project with any complexity and is likely responsible for getting my foot in the door at my current job. For this reason I am willing to continue tending the garden it occupies.</p>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/minecraftle-v1.1</guid>
            <pubDate>2023-01-26</pubDate>
        </item>
        

    </channel>
</rss>