<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>FooSoft Productions</title><link>https://foosoft.net</link><description/><managingEditor>alex@foosoft.net (Alex Yatskov)</managingEditor><item><title>Kanji Frequency in Wikipedia and Novels</title><link>https://foosoft.net/posts/kanji-frequency/index.html</link><description>Procedurally analyzing large bodies of text to understand kanji prevalence.</description><content:encoded><![CDATA[<p>I love learning new kanji, probably too much, in fact. Once I finished <a href="http://en.wikipedia.org/wiki/Remembering_the_Kanji">Remembering the Kanji</a> Volumes 1 and 3, I couldn&rsquo;t just quit! I had to find more characters to study! After memorizing another meager 25 or so kanji to catch up to the proposed revision of the 常用漢字 standard, I tried to find good frequency lists on the internet, but none were very extensive and all had relatively limited sample sizes.</p>
<p>At some point, an idea came to me that the Japanese Wikipedia would be a really good source of data. It could be downloaded for offline processing and obviously includes every possible topic. I whipped up a couple of Perl scripts to process the 20 gigabytes or so of data. About six hours later (maybe I should have done this in C++?), I had a nice sorted table of characters ready for processing. I wrote a couple more scripts to generate reports that applied the frequency data to the RTK1, RTK3, 常用漢字 (old and new), and finally the 人名用漢字 (just for fun) character lists.</p>
<p>After I completed my analysis of Wikipedia, I ran a slightly modified version of my script on several gigabytes of assorted novels in raw text format. I&rsquo;m also including this data as it is another great indicator of actual Japanese language usage. Depending on what kind of text you are planning on reading, this report may even be more suitable than the original Wikipedia analysis.</p>
<p>These reports span over 500 million characters (over 20k of which are unique). This is obviously a tremendous amount of data. Let me know if you want to see something else included in the reports about this data that isn&rsquo;t yet!</p>
<h2 id="frequency-reports">Frequency Reports</h2>
<ul>
<li><strong>Wikipedia</strong>
<ul>
<li>Reports for characters appearing at least
<a href="dl/report1.tar.gz">1</a>,
<a href="dl/report10.tar.gz">10</a>,
<a href="dl/report100.tar.gz">100</a>,
<a href="dl/report1000.tar.gz">1000</a>, or
<a href="dl/report10000.tar.gz">10000</a> times.</li>
<li>All of the Wikipedia reports in <a href="dl/reports.tar.gz">compressed format</a> (from the above links).</li>
<li>Generator <a href="dl/scripts.tar.gz">Perl scripts and raw data</a> that were used to process this data.</li>
</ul>
</li>
<li><strong>Novels</strong>
<ul>
<li>Alternate sample report on <a href="dl/novel/report.tar.gz">text from hundreds of novels</a>.</li>
</ul>
</li>
</ul>
]]></content:encoded><guid>https://foosoft.net/posts/kanji-frequency/index.html</guid><pubDate>Sat, 20 Feb 2010 00:00:00 -0700</pubDate></item><item><title>Installing Diablo II on Linux</title><link>https://foosoft.net/posts/installing-diablo-ii-on-linux/index.html</link><description>Installing Diablo II with the GLIDE3-to-OpenGL-Wrapper and PlugY on Linux using the original disk ISOs.</description><content:encoded><![CDATA[<p>Originally released in 2000, Diablo II can be a hassle to get up and running on any modern operating system. The setup process is even more involved if you wish to make the game look its best via the <a href="http://www.svenswrapper.de/english/">GLIDE3-to-OpenGL-Wrapper</a>, while enabling online-only features with <a href="http://plugy.free.fr/en/index.html">PlugY</a> in single-player mode&hellip; all on Linux, using images of the original disks!</p>
<h2 id="install-base-game-and-expansion">Install Base Game and Expansion</h2>
<ol>
<li>Install Wine:
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>sudo apt-get install wine <span style="color:#8b949e;font-style:italic"># for Ubuntu</span>
</span></span></code></pre><pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>sudo dnf install wine <span style="color:#8b949e;font-style:italic"># for Fedora</span>
</span></span></code></pre></li>
<li>Make a virtual CD-ROM directory in your home directory:
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>mkdir ~/cdrom
</span></span></code></pre></li>
<li>Mount the Diablo II install disk image:
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>sudo mount -o loop /path/to/install.iso ~/cdrom
</span></span></code></pre></li>
<li>Run <code>install.exe</code> from the CD-ROM, and select &ldquo;Full Install&rdquo; when prompted. <br>
<img src="./img/d2-install.png" alt="Diablo II installation options"></li>
<li>When prompted to do so, unmount the currently inserted disk:
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>sudo umount -f ~/cdrom
</span></span></code></pre></li>
<li>Proceed to mount and unmount disk images as needed until installation is complete.</li>
<li>The installer will prompt you to run the video test; skip this step for now. <br>
<img src="./img/video-test.png" alt="Diablo II video test prompt"></li>
<li>Mount the expansion disk image, and install Diablo II: Lord of Destruction: <br>
<img src="./img/d2lod-install.png" alt="Diablo II: Lord of Destruction installation options"></li>
<li>Copy over <code>d2xvideo.mpq</code> and <code>d2xmusic.mpq</code> from the expansion CD-ROM to your game install directory.</li>
<li>Update to the latest version of the game (1.14d at the time of this writing).</li>
</ol>
<h2 id="install-glide3-to-opengl-wrapper">Install GLIDE3-to-OpenGL-Wrapper</h2>
<ol>
<li>Download and install <a href="http://www.svenswrapper.de/english/">GLIDE3-to-OpenGL-Wrapper</a>.</li>
<li>Extract the glide wrapper to your game install directory and run <code>glide-init.exe</code>.</li>
<li>Change the language to English if necessary by clicking the second-to-last button on the left. <br>
<img src="./img/glide-wrapper.png" alt="GLIDE3-to-OpenGL-Wrapper settings"></li>
<li>Under the &ldquo;Extensions&rdquo; section, make sure the &ldquo;WGL_ARB_render_texture&rdquo; checkbox is ticked.</li>
<li>Under the &ldquo;Settings&rdquo; section, tick the <em>desktopresolution</em> checkbox.</li>
<li>Under the &ldquo;Renderer&rdquo; section, tick the &ldquo;no gamma&rdquo; and &ldquo;supersampling&rdquo; checkboxes.</li>
<li>Run <code>D2VidTst.exe</code> from the game directory and select &ldquo;3dfx Glide&rdquo;: <br>
<img src="./img/video-test-glide.png" alt="The 3dfx Glide option in the Diablo II video test"></li>
</ol>
<h2 id="install-plugy">Install PlugY</h2>
<ol>
<li>Download and install <a href="http://plugy.free.fr/en/index.html">PlugY</a>. <br>
<img src="./img/plugy.png" alt="PlugY installation options"></li>
<li>Edit the <code>PlugY.ini</code> file in the <code>Mod PlugY</code> directory; in the <code>LAUNCHING</code> section, set <code>Param=--3dfx</code>.</li>
<li>Run <code>PlugY.exe</code> from the <code>Mod PlugY</code> directory to start the game (don&rsquo;t use <code>Game.exe</code> directly).</li>
<li>Don&rsquo;t forget to change the resolution from 640x480 to 800x600 in the settings menu! <br>
<img src="./img/d2-settings.png" alt="Diablo II video-resolution settings"></li>
</ol>
]]></content:encoded><guid>https://foosoft.net/posts/installing-diablo-ii-on-linux/index.html</guid><pubDate>Sat, 28 Apr 2018 00:00:00 -0700</pubDate></item><item><title>Decrapifying the Twitter Timeline</title><link>https://foosoft.net/posts/decrapifying-the-twitter-timeline/index.html</link><description>Taking a look at a couple of uBlock rules to block Twitter's forced engagement mechanism.</description><content:encoded><![CDATA[<p>If you have been using <a href="https://twitter.com">Twitter</a> recently, you may have noticed that other people&rsquo;s &ldquo;likes&rdquo; are now showing up in your timeline. To make matters even worse, you will get notifications about these &ldquo;likes&rdquo; even for people whose retweets you have blocked&hellip; Fortunately, you can stop this nonsense and revert the timeline to its old behavior if you use the <a href="https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm">uBlock Origin</a> browser extension.</p>
<ol>
<li>Open the uBlock settings page.</li>
<li>Select the &ldquo;My filters&rdquo; tab.</li>
<li>Add the following filters:
<pre><code>twitter.com##.tweet[data-component-context=&quot;suggest_activity_tweet&quot;]
twitter.com##.tweet[data-component-context=&quot;suggest_recycled_tweet_inline&quot;]
twitter.com##.tweet[data-component-context=&quot;suggest_pyle_tweet&quot;]
</code></pre>
</li>
<li>Enjoy a timeline with less noise!</li>
</ol>
]]></content:encoded><guid>https://foosoft.net/posts/decrapifying-the-twitter-timeline/index.html</guid><pubDate>Sun, 22 Jul 2018 00:00:00 -0700</pubDate></item><item><title>Working With Patches in Git</title><link>https://foosoft.net/posts/working-with-patches-in-git/index.html</link><description>Generating and applying branch-agnostic patches for debug code in Git.</description><content:encoded><![CDATA[<p>As software developers, we often make local code changes for debugging, logging, and more. Oftentimes we are unable to commit these changes to the master code as they may make the application run slower, or even render it unusable for certain use cases. For lots of people, that means the hacks have to be re-added every time they wish to debug something. This is time-consuming and error-prone; fortunately, Git contains all the tools needed to solve this problem.</p>
<p>Let&rsquo;s start by creating a new repository for <code>adder</code>, a simple tool that adds two numbers:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>git init adder
</span></span></code></pre><p>Now let&rsquo;s add <code>main.py</code>, the file containing the full source of our application:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">#!/usr/bin/env python</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">os</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">sys</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">add</span>(a, b):
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">+</span> b
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">main</span>(args):
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span>:
</span></span><span style="display:flex;"><span>        sum <span style="color:#ff7b72;font-weight:bold">=</span> add(int(args[<span style="color:#a5d6ff">0</span>]), int(args[<span style="color:#a5d6ff">1</span>]))
</span></span><span style="display:flex;"><span>        print(<span style="color:#79c0ff">f</span><span style="color:#a5d6ff">&#39;the sum is </span><span style="color:#a5d6ff">{</span>sum<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">except</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#a5d6ff">&#39;invalid arguments&#39;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#ff7b72;font-weight:bold">.</span>exit(<span style="color:#a5d6ff">2</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">if</span> <span style="color:#79c0ff">__name__</span> <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">&#39;__main__&#39;</span>:
</span></span><span style="display:flex;"><span>    main(sys<span style="color:#ff7b72;font-weight:bold">.</span>argv[<span style="color:#a5d6ff">1</span>:])
</span></span></code></pre><p>Next, we stage and commit the change:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>git add main.py
</span></span><span style="display:flex;"><span>git commit -m <span style="color:#a5d6ff">&#34;Add application to add numbers&#34;</span>
</span></span></code></pre><p>Now let&rsquo;s pretend that there is some sort of regression and we must add logging to investigate the issue. First, we need to add our custom logging library located in a new file, <code>logging.py</code>:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">info</span>(msg):
</span></span><span style="display:flex;"><span>    print(msg)
</span></span></code></pre><p>We must also update the existing <code>main.py</code> file to call into this library:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">#!/usr/bin/env python</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">os</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">sys</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">logging</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">add</span>(a, b):
</span></span><span style="display:flex;"><span>    logging<span style="color:#ff7b72;font-weight:bold">.</span>info(<span style="color:#79c0ff">f</span><span style="color:#a5d6ff">&#39;adding numbers </span><span style="color:#a5d6ff">{</span>a<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> and </span><span style="color:#a5d6ff">{</span>b<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">+</span> b
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">main</span>(args):
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span>:
</span></span><span style="display:flex;"><span>        sum <span style="color:#ff7b72;font-weight:bold">=</span> add(int(args[<span style="color:#a5d6ff">0</span>]), int(args[<span style="color:#a5d6ff">1</span>]))
</span></span><span style="display:flex;"><span>        print(<span style="color:#79c0ff">f</span><span style="color:#a5d6ff">&#39;the sum is </span><span style="color:#a5d6ff">{</span>sum<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">except</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#a5d6ff">&#39;invalid arguments&#39;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#ff7b72;font-weight:bold">.</span>exit(<span style="color:#a5d6ff">2</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">if</span> <span style="color:#79c0ff">__name__</span> <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">&#39;__main__&#39;</span>:
</span></span><span style="display:flex;"><span>    main(sys<span style="color:#ff7b72;font-weight:bold">.</span>argv[<span style="color:#a5d6ff">1</span>:])
</span></span></code></pre><p>With our local changes complete, let&rsquo;s review the differences against the <code>master</code> branch:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>git diff
</span></span></code></pre><p>The output of the above command is a valid patch file in and of itself:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#79c0ff;font-weight:bold">index 7efa5d6..d3aadba 100755
</span></span></span><span style="display:flex;"><span><span style="color:#ffa198;background-color:#490202">--- a/main.py
</span></span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+++ b/main.py
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff">@@ -3,2 +3,3 @@
</span></span></span><span style="display:flex;"><span> import os
</span></span><span style="display:flex;"><span> import sys
</span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+import logging
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff">@@ -6,2 +7,3 @@
</span></span></span><span style="display:flex;"><span> def add(a, b):
</span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+    logging.info(f&#39;adding numbers {a} and {b}&#39;)
</span></span></span><span style="display:flex;"><span>     return a + b
</span></span></code></pre><p>While the changes to <code>main.py</code> are shown, the contents of the newly added <code>logging.py</code> file are not. To fix this, simply add it with:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>git add --intent-to-add logging.py
</span></span></code></pre><p>With this, both files are now correctly shown as changes:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#79c0ff;font-weight:bold">diff --git a/logging.py b/logging.py
</span></span></span><span style="display:flex;"><span>new file mode 100644
</span></span><span style="display:flex;"><span><span style="color:#79c0ff;font-weight:bold">index 0000000..39b4bbc
</span></span></span><span style="display:flex;"><span><span style="color:#ffa198;background-color:#490202">--- /dev/null
</span></span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+++ b/logging.py
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff">@@ -0,0 +1,2 @@
</span></span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+def info(msg):
</span></span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+    print(msg)
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff;font-weight:bold">diff --git a/main.py b/main.py
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff;font-weight:bold">index 7efa5d6..d3aadba 100755
</span></span></span><span style="display:flex;"><span><span style="color:#ffa198;background-color:#490202">--- a/main.py
</span></span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+++ b/main.py
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff">@@ -3,2 +3,3 @@
</span></span></span><span style="display:flex;"><span> import os
</span></span><span style="display:flex;"><span> import sys
</span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+import logging
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff">@@ -6,2 +7,3 @@
</span></span></span><span style="display:flex;"><span> def add(a, b):
</span></span><span style="display:flex;"><span><span style="color:#56d364;background-color:#0f5323">+    logging.info(f&#39;adding numbers {a} and {b}&#39;)
</span></span></span><span style="display:flex;"><span>     return a + b
</span></span></code></pre><p>With the changes looking good, it&rsquo;s time to generate the patch file:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>git diff --output ~/logging.patch
</span></span></code></pre><p>Now that we have performed the required debugging and saved the day, we can revert all of our local debugging changes:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>git reset --hard
</span></span></code></pre><p>Let&rsquo;s now pretend that some time has passed and we must debug the <code>add</code> function once again. Furthermore, as a result of feature creep, the <code>adder</code> tool now also has to handle subtraction. The updated <code>main.py</code> file is shown below:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">os</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">sys</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">add</span>(a, b):
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">+</span> b
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">main</span>(args):
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span>:
</span></span><span style="display:flex;"><span>        op <span style="color:#ff7b72;font-weight:bold">=</span> args[<span style="color:#a5d6ff">0</span>]
</span></span><span style="display:flex;"><span>        a <span style="color:#ff7b72;font-weight:bold">=</span> int(args[<span style="color:#a5d6ff">1</span>])
</span></span><span style="display:flex;"><span>        b <span style="color:#ff7b72;font-weight:bold">=</span> int(args[<span style="color:#a5d6ff">2</span>])
</span></span><span style="display:flex;"><span>        ops <span style="color:#ff7b72;font-weight:bold">=</span> {<span style="color:#a5d6ff">&#39;+&#39;</span>: add, <span style="color:#a5d6ff">&#39;-&#39;</span>: subtract}
</span></span><span style="display:flex;"><span>        result <span style="color:#ff7b72;font-weight:bold">=</span> ops[op](a, b)
</span></span><span style="display:flex;"><span>        print(<span style="color:#79c0ff">f</span><span style="color:#a5d6ff">&#39;the result is </span><span style="color:#a5d6ff">{</span>result<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">except</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#a5d6ff">&#39;invalid arguments&#39;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#ff7b72;font-weight:bold">.</span>exit(<span style="color:#a5d6ff">2</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">subtract</span>(a, b):
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">-</span> b
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">if</span> <span style="color:#79c0ff">__name__</span> <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">&#39;__main__&#39;</span>:
</span></span><span style="display:flex;"><span>    main(sys<span style="color:#ff7b72;font-weight:bold">.</span>argv[<span style="color:#a5d6ff">1</span>:])
</span></span></code></pre><p>As there are no conflicts in the modified chunks, we can now apply the patch:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>git apply ~/logging.patch
</span></span></code></pre><p>We can see that the <code>logging.py</code> file is created and <code>main.py</code> has been updated with the logging from before:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">os</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">sys</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> <span style="color:#ff7b72">logging</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">add</span>(a, b):
</span></span><span style="display:flex;"><span>    logging<span style="color:#ff7b72;font-weight:bold">.</span>info(<span style="color:#79c0ff">f</span><span style="color:#a5d6ff">&#39;adding numbers </span><span style="color:#a5d6ff">{</span>a<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> and </span><span style="color:#a5d6ff">{</span>b<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">+</span> b
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">main</span>(args):
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span>:
</span></span><span style="display:flex;"><span>        op <span style="color:#ff7b72;font-weight:bold">=</span> args[<span style="color:#a5d6ff">0</span>]
</span></span><span style="display:flex;"><span>        a <span style="color:#ff7b72;font-weight:bold">=</span> int(args[<span style="color:#a5d6ff">1</span>])
</span></span><span style="display:flex;"><span>        b <span style="color:#ff7b72;font-weight:bold">=</span> int(args[<span style="color:#a5d6ff">2</span>])
</span></span><span style="display:flex;"><span>        ops <span style="color:#ff7b72;font-weight:bold">=</span> {<span style="color:#a5d6ff">&#39;+&#39;</span>: add, <span style="color:#a5d6ff">&#39;-&#39;</span>: subtract}
</span></span><span style="display:flex;"><span>        result <span style="color:#ff7b72;font-weight:bold">=</span> ops[op](a, b)
</span></span><span style="display:flex;"><span>        print(<span style="color:#79c0ff">f</span><span style="color:#a5d6ff">&#39;the result is </span><span style="color:#a5d6ff">{</span>result<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">except</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#a5d6ff">&#39;invalid arguments&#39;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#ff7b72;font-weight:bold">.</span>exit(<span style="color:#a5d6ff">2</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">def</span> <span style="color:#d2a8ff;font-weight:bold">subtract</span>(a, b):
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">-</span> b
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">if</span> <span style="color:#79c0ff">__name__</span> <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">&#39;__main__&#39;</span>:
</span></span><span style="display:flex;"><span>    main(sys<span style="color:#ff7b72;font-weight:bold">.</span>argv[<span style="color:#a5d6ff">1</span>:])
</span></span></code></pre><p>If you do run into conflicts, you can use the <a href="https://git-scm.com/docs/git-apply#Documentation/git-apply.txt---reject">reject</a> flag to apply as many changes as possible without failing the entire operation.</p>
]]></content:encoded><guid>https://foosoft.net/posts/working-with-patches-in-git/index.html</guid><pubDate>Sun, 12 Jun 2022 00:00:00 -0700</pubDate></item><item><title>Squashing All Commits in a Git Topic Branch</title><link>https://foosoft.net/posts/squashing-all-commits-in-a-git-topic-branch/index.html</link><description>How to squash all commits in a topic branch without interactive pick/squash selection.</description><content:encoded><![CDATA[<p>When all work in a <a href="https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch">topic branch</a> is complete and we are ready to bring the changes back to the <code>master</code> branch (or open a pull request), it may be a good idea to clean up the commit history first. This is especially true if your commits are full of messages like &ldquo;WIP&rdquo;, &ldquo;fixing&rdquo;, &ldquo;progress&rdquo;, etc. Even more frequently, the individual topic commits are nothing but noise and you simply want to squash all of them.</p>
<p>Let&rsquo;s assume that this is our commit log:</p>
<pre><code class="language-git">commit c9e1e685ff81b1a6ec75848423219caa752838c4 (HEAD -&gt; topic)
Author: Alex Yatskov &lt;alex@foosoft.net&gt;
Date:   Sun Jul 17 10:37:05 2022 -0700

    Progress

commit 4ad391d17d92fe499247ac848556a3fd72842621
Author: Alex Yatskov &lt;alex@foosoft.net&gt;
Date:   Sun Jul 17 10:36:49 2022 -0700

    Fixing

commit bef920b9789d988507b1972de697273ea2df3b24
Author: Alex Yatskov &lt;alex@foosoft.net&gt;
Date:   Sun Jul 17 10:36:39 2022 -0700

    WIP

commit 9e2d24395008af39c6192df128bcbce0deaa2732 (master)
Author: Alex Yatskov &lt;alex@foosoft.net&gt;
Date:   Sun Jul 17 10:36:09 2022 -0700

    Mainline feature
</code></pre>
<p>Typically, to squash the topic commits, you might execute the following:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>git rebase -i 9e2d24395008af39c6192df128bcbce0deaa2732
</span></span></code></pre><p>This command will open a text editor, prompting you to specify which commits to pick and which to squash:</p>
<pre><code>pick bef920b WIP
pick 4ad391d Fixing
pick c9e1e68 Progress

# Rebase 9e2d243..c9e1e68 onto 9e2d243 (3 commands)
#
# Commands:
# p, pick &lt;commit&gt; = use commit
# r, reword &lt;commit&gt; = use commit, but edit the commit message
# e, edit &lt;commit&gt; = use commit, but stop for amending
# s, squash &lt;commit&gt; = use commit, but meld into previous commit
# f, fixup [-C | -c] &lt;commit&gt; = like &quot;squash&quot; but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec &lt;command&gt; = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop &lt;commit&gt; = remove commit
# l, label &lt;label&gt; = label current HEAD with a name
# t, reset &lt;label&gt; = reset HEAD to a label
# m, merge [-C &lt;commit&gt; | -c &lt;commit&gt;] &lt;label&gt; [# &lt;oneline&gt;]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified); use -c &lt;commit&gt; to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
</code></pre>
<p>You would then pick the first commit and squash the rest:</p>
<pre><code>pick bef920b WIP
squash 4ad391d Fixing
squash c9e1e68 Progress
...
</code></pre>
<p>There is nothing wrong with this approach, but it gets cumbersome if you have dozens of commits. A quicker way would be to reset the branch to the start of the topic changes, and re-commit as follows:</p>
<pre><code>git reset --soft 9e2d24395008af39c6192df128bcbce0deaa2732
git commit -m &quot;Everything required for my new feature&quot;
</code></pre>
<p>This operation has the advantage of being non-interactive and produces the same result (at the cost of flexibility in specifying what to squash). If you are like me and prefer to squash all topic commits anyway, this may be an improvement to your Git workflow.</p>
]]></content:encoded><guid>https://foosoft.net/posts/squashing-all-commits-in-a-git-topic-branch/index.html</guid><pubDate>Sat, 16 Jul 2022 00:00:00 -0700</pubDate></item><item><title>Saying Goodbye to Vimscript</title><link>https://foosoft.net/posts/saying-goodbye-to-vimscript/index.html</link><description>A walkthrough for completely migrating a Neovim configuration from Vimscript to Lua.</description><content:encoded><![CDATA[<p>After using Neovim exclusively for the past several years, I&rsquo;ve decided it&rsquo;s finally time to migrate all my configuration files from Vimscript to Lua. I no longer wish to remember the syntax peculiarities of an archaic language not used anywhere else. Similarly, I can&rsquo;t think of a reason to ever return to vanilla Vim. In this post, I summarize what needs to be done to switch to Lua for others thinking of taking the dive.</p>
<h2 id="file-layout">File Layout</h2>
<p>To get started with Lua, replace your <code>init.vim</code> file with <code>init.lua</code>. Note that if you have both files present at once, Neovim will complain and your configuration will not load. The scripts contained in the <code>ftplugin</code> directory can also be converted to Lua in the same way. Finally, it is also possible to split the configuration into multiple files by creating a <code>lua</code> subdirectory and using <code>require</code> to import scripts contained therein.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;common&#39;</span>)
</span></span><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;lsp&#39;</span>)
</span></span><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;plugins&#39;</span>)
</span></span><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;util&#39;</span>)
</span></span><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;autocmd&#39;</span>)
</span></span></code></pre><p>With these changes, your directory structure may look similar to this:</p>
<pre><code>├── ftplugin
│   ├── c.lua
│   ├── cpp.lua
│   ├── go.lua
│   ├── html.lua
│   ├── markdown.lua
│   └── yaml.lua
├── init.lua
├── lua
│   ├── autocmd.lua
│   ├── common.lua
│   ├── ginit.lua
│   ├── lsp.lua
│   ├── plugins.lua
│   └── util.lua
└── pack
    └── plugins
</code></pre>
<p>If you use <a href="https://github.com/equalsraf/neovim-qt">Neovim Qt</a>, you may be wondering about the presence of <code>ginit.lua</code> from the above file listing. Unfortunately, Neovim Qt does not support a Lua-only configuration path and insists on using <code>ginit.vim</code> for GUI-specific configuration. Furthermore, attempting to include Neovim Qt-specific commands in <code>init.lua</code> will cause errors on load (tests for GUI presence via <code>has</code> do not appear to work). Fortunately, we can use an autocommand to conditionally <code>require</code> the <code>ginit.lua</code> file only when running under a GUI.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>vim.api.nvim_create_autocmd(
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#39;UIEnter&#39;</span>, {
</span></span><span style="display:flex;"><span>        callback <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">function</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">if</span> vim.v.event.chan <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">1</span> <span style="color:#ff7b72">then</span>
</span></span><span style="display:flex;"><span>                require(<span style="color:#a5d6ff">&#39;ginit&#39;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">end</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">end</span>,
</span></span><span style="display:flex;"><span>        once <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>,
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>)
</span></span></code></pre><h2 id="color-scheme-and-leader">Color Scheme and Leader</h2>
<p>The color scheme can be set by executing the same <code>colorscheme</code> command as in Vim. There are two different ways to do this, with the latter looking a bit cleaner than the former. This pattern of treating the command name as a callable method in <code>cmd</code> extends to other commands (including ones exposed by plugins) as well.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>vim.cmd(<span style="color:#a5d6ff">&#39;colorscheme solarized8&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- or with some syntax sugar...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>vim.cmd.colorscheme(<span style="color:#a5d6ff">&#39;solarized8&#39;</span>)
</span></span></code></pre><p>The leader key is configured with a global variable, just like in Vim.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>vim.g.mapleader <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39; &#39;</span>
</span></span></code></pre><h2 id="core-options">Core Options</h2>
<p>Consider comparing your configuration against the <a href="https://neovim.io/doc/user/options.html">defaults</a> listed in the documentation. Neovim ships with mostly sane defaults and you can reduce the size of your configuration file by skipping redundant settings. Note that boolean settings in Vim that start with no when <code>false</code> (such as <code>nonumber</code>) are represented by assignments instead.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>vim.opt.autowrite <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>vim.opt.completeopt <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;menuone,noselect&#39;</span>
</span></span><span style="display:flex;"><span>vim.opt.expandtab <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>vim.opt.fileformats <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;unix,dos,mac&#39;</span>
</span></span><span style="display:flex;"><span>vim.opt.foldenable <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>vim.opt.guicursor <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;n:blinkon0&#39;</span>
</span></span><span style="display:flex;"><span>vim.opt.ignorecase <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>vim.opt.linebreak <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>vim.opt.modeline <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>vim.opt.modelines <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>
</span></span><span style="display:flex;"><span>vim.opt.number <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>vim.opt.shiftround <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>vim.opt.shiftwidth <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">4</span>
</span></span><span style="display:flex;"><span>vim.opt.showmode <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>vim.opt.smartcase <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>vim.opt.swapfile <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>vim.opt.tabstop <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">4</span>
</span></span><span style="display:flex;"><span>vim.opt.termguicolors <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>vim.opt.updatetime <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">300</span>
</span></span><span style="display:flex;"><span>vim.opt.wrap <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>vim.opt.writebackup <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">false</span>
</span></span></code></pre><h2 id="plugin-configuration">Plugin Configuration</h2>
<p>Plugins written for Vim are generally configured with global variables; these are exposed in the <code>vim.g</code> table. Vimscript supports namespaces via the <code>#</code> character, which can cause syntax issues for Lua. To work around this problem, use the square bracket index operator on the table instead.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- vim-airline</span>
</span></span><span style="display:flex;"><span>vim.g[<span style="color:#a5d6ff">&#39;airline#extensions#tabline#enabled&#39;</span>] <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">1</span>
</span></span><span style="display:flex;"><span>vim.g[<span style="color:#a5d6ff">&#39;airline_symbols_ascii&#39;</span>] <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- vim-dirvish</span>
</span></span><span style="display:flex;"><span>vim.g.dirvish_mode <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;:sort ,^.*[</span><span style="color:#79c0ff">\\</span><span style="color:#a5d6ff">/],&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- vim-go</span>
</span></span><span style="display:flex;"><span>vim.g.go_diagnostics_enabled <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>
</span></span><span style="display:flex;"><span>vim.g.go_imports_autosave <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>
</span></span><span style="display:flex;"><span>vim.g.go_metalinter_enabled <span style="color:#ff7b72;font-weight:bold">=</span> {}
</span></span><span style="display:flex;"><span>vim.g.go_null_module_warning <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>
</span></span><span style="display:flex;"><span>vim.g.go_version_warning <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>
</span></span></code></pre><h2 id="key-mappings">Key Mappings</h2>
<p>Key mappings are also reasonably straightforward. The main difference from vanilla Vim is that commands must start with a <code>&lt;cmd&gt;</code> tag instead of the <code>:</code> prefix. When defining key mappings for a combination of modes (such as normal and visual), the mode parameter has to be specified as a Lua table; a comma-delimited string will not work.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- common keymaps</span>
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;i&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-c&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;esc&gt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;bs&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;cmd&gt;bd&lt;cr&gt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-c&gt;&lt;c-c&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;cmd&gt;nohlsearch&lt;cr&gt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;leader&gt;&lt;leader&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;cmd&gt;b#&lt;cr&gt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;leader&gt;w&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;cmd&gt;w&lt;cr&gt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;leader&gt;x&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;cmd&gt;x&lt;cr&gt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;j&#39;</span>, <span style="color:#a5d6ff">&#39;gj&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;k&#39;</span>, <span style="color:#a5d6ff">&#39;gk&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- clipboard keymaps</span>
</span></span><span style="display:flex;"><span>vim.keymap.set({<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;v&#39;</span>}, <span style="color:#a5d6ff">&#39;&lt;leader&gt;P&#39;</span>, <span style="color:#a5d6ff">&#39;&#34;+P&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set({<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;v&#39;</span>}, <span style="color:#a5d6ff">&#39;&lt;leader&gt;Y&#39;</span>, <span style="color:#a5d6ff">&#39;&#34;+y$&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set({<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;v&#39;</span>}, <span style="color:#a5d6ff">&#39;&lt;leader&gt;d&#39;</span>, <span style="color:#a5d6ff">&#39;&#34;+d&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set({<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;v&#39;</span>}, <span style="color:#a5d6ff">&#39;&lt;leader&gt;d&#39;</span>, <span style="color:#a5d6ff">&#39;&#34;+dd&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set({<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;v&#39;</span>}, <span style="color:#a5d6ff">&#39;&lt;leader&gt;p&#39;</span>, <span style="color:#a5d6ff">&#39;&#34;+p&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set({<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;v&#39;</span>}, <span style="color:#a5d6ff">&#39;&lt;leader&gt;y&#39;</span>, <span style="color:#a5d6ff">&#39;&#34;+y&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set({<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;v&#39;</span>}, <span style="color:#a5d6ff">&#39;&lt;leader&gt;yy&#39;</span>, <span style="color:#a5d6ff">&#39;&#34;+yy&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- split keymaps</span>
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;a-=&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;&lt;c-=&gt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;a-h&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;&lt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;a-j&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;+&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;a-k&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;-&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;a-l&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;&gt;&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-h&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;h&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-j&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;j&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-k&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;k&#39;</span>)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-l&gt;&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;c-w&gt;l&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- fzf.vim keymaps</span>
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;leader&gt;fg&#39;</span>, vim.cmd.GFiles)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;leader&gt;fh&#39;</span>, vim.cmd.History)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;leader&gt;fb&#39;</span>, vim.cmd.Buffers)
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#a5d6ff">&#39;n&#39;</span>, <span style="color:#a5d6ff">&#39;&lt;leader&gt;fl&#39;</span>, vim.cmd.Lines)
</span></span></code></pre><h2 id="auto-commands">Auto Commands</h2>
<p>Autocommands are defined as a list of conditions followed by a table specifying what you want the action to do. Unlike key mappings, it is possible to declare the condition list as either a table or a comma-delimited string.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>vim.api.nvim_create_autocmd(
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#39;BufRead,BufNewFile&#39;</span>, {
</span></span><span style="display:flex;"><span>        pattern <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;*.gohtml&#39;</span>,
</span></span><span style="display:flex;"><span>        command <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;set filetype=html&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- or using the table syntax...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>vim.api.nvim_create_autocmd(
</span></span><span style="display:flex;"><span>    {<span style="color:#a5d6ff">&#39;BufRead&#39;</span>, <span style="color:#a5d6ff">&#39;BufNewFile&#39;</span>}, {
</span></span><span style="display:flex;"><span>        pattern <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;*.gohtml&#39;</span>,
</span></span><span style="display:flex;"><span>        command <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;set filetype=html&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>)
</span></span></code></pre><h2 id="useful-resources">Useful Resources</h2>
<ul>
<li><a href="http://www.lua.org/manual/5.4/">http://www.lua.org/manual/5.4/</a></li>
<li><a href="https://github.com/nanotee/nvim-lua-guide">https://github.com/nanotee/nvim-lua-guide</a></li>
<li><a href="https://neovim.io/doc/user/api.html">https://neovim.io/doc/user/api.html</a></li>
<li><a href="https://neovim.io/doc/user/lua.html">https://neovim.io/doc/user/lua.html</a></li>
</ul>
]]></content:encoded><guid>https://foosoft.net/posts/saying-goodbye-to-vimscript/index.html</guid><pubDate>Sat, 12 Nov 2022 00:00:00 -0700</pubDate></item><item><title>Fixing Firefox Video Playback in Fedora 37</title><link>https://foosoft.net/posts/fixing-firefox-video-playback-in-fedora-37/index.html</link><description>Figuring out how to get Firefox video playback working in Fedora again.</description><content:encoded><![CDATA[<p>After upgrading to <a href="https://docs.fedoraproject.org/en-US/releases/f37/">Fedora 37</a>, I noticed that some videos in Firefox were no longer playing. Instead, the error code 224002 was displayed. I have been using Fedora for years and have never encountered this problem. After some research, it looked like I should enable the OpenH264 Video Codec in the plugin manager and wait for it to download.</p>
<p><img src="img/codec.png" alt="OpenH264 Video Codec listed in the Firefox plugin manager"></p>
<p>Done and done&hellip; Except the codec was not downloading no matter how long I waited. Restarting Firefox and deactivating/reactivating the video codec were also not helping. Turns out that the fix is to install the <code>mozilla-openh264</code> package yourself:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>sudo dnf install mozilla-openh264
</span></span></code></pre><p>Not a big deal, but I could have done without the deceptive messaging, Firefox.</p>
]]></content:encoded><guid>https://foosoft.net/posts/fixing-firefox-video-playback-in-fedora-37/index.html</guid><pubDate>Fri, 18 Nov 2022 00:00:00 -0700</pubDate></item><item><title>Sunsetting the Yomichan Project</title><link>https://foosoft.net/posts/sunsetting-the-yomichan-project/index.html</link><description>Winding down the Yomichan ecosystem after over a decade of development.</description><content:encoded><![CDATA[<p><em>Update: Yomitan, a community fork of Yomichan, has reached a stable release; see the <a href="/posts/passing-the-torch-to-yomitan">related post</a> for details.</em></p>
<p>Fellow Japanese learning friends, after careful consideration, I&rsquo;ve made the decision to cease development of Yomichan and related tools: Yomichan Import, Zero-EPWING, and JMDict. The Anki-Connect project will continue to be supported for the foreseeable future.</p>
<p>The original Yomichan for Anki project had its beginnings back in 2011, over a decade ago. I began the project shortly after embarking on my <a href="http://www.alljapaneseallthetime.com/blog/">AJATT</a> Japanese language journey. It proved to be an invaluable tool for mining vocabulary from books I was struggling to read at the time. Over the years, and with feedback from the folks at the (now defunct) Kanji Koohii Forums, it evolved into the browser extension for Chrome and Firefox that you know today. I like to think that Yomichan accepted the torch passed down to it by <a href="https://rikaisama.sourceforge.net/">Rikaisama</a>, another popular extension for power users learning Japanese during this period.</p>
<p>I have since then attained the N1 <a href="https://www.jlpt.jp/e/index.html">JLPT</a> certification (still one of my proudest achievements), lived in Japan, and graduated with a master&rsquo;s degree from <a href="https://www.sfc.keio.ac.jp/en/">Keio University</a>. I speak, read and write Japanese every day. I stopped doing flash cards in Anki years ago. I now prefer to learn the language in a more organic, contextual way. I can&rsquo;t remember the last time I used Yomichan in my browser other than to test code changes.</p>
<p>I know that I do my best work on open source projects that answer my immediate needs. If I don&rsquo;t make this distinction, then they just become a second (or third, or fourth&hellip;) job. Yomichan no longer meets this criterion for me, and thus it is time to let go.</p>
<h2 id="dear-yomichan-users">Dear Yomichan Users</h2>
<p>Thank you for your support and feedback over all of these years. Yomichan will remain available in the browser extension stores indefinitely. You are free to continue using it as long as you find it useful or until you find something better. It will never be sold off to run ads and steal your data. Web technology moves quickly, and I suppose that at some point Yomichan will probably stop functioning. Please enjoy the extension and related tooling until then!</p>
<h2 id="dear-yomichan-developers">Dear Yomichan Developers</h2>
<p>Deepest thanks to <a href="https://github.com/toasted-nutbread">toasted-nutbread</a>, <a href="https://github.com/siikamiika">siikamiika</a>, and all of the <a href="https://github.com/FooSoft/yomichan/graphs/contributors">other talented developers</a> who have contributed countless hours to improving the Yomichan ecosystem. You are absolute legends, and these projects would not be the same without you. If there is interest in continuing development, please fork the project and shape it to your needs. I will happily link to your work to help get the word out. I have just two requests:</p>
<ul>
<li>Create a new title for your fork to avoid confusion.</li>
<li>Respect the license and make proper attribution.</li>
</ul>
<p>I&rsquo;m looking forward to seeing what will come next!</p>
]]></content:encoded><guid>https://foosoft.net/posts/sunsetting-the-yomichan-project/index.html</guid><pubDate>Sat, 25 Feb 2023 00:00:00 -0700</pubDate></item><item><title>Generating the FooSoft.net Homepage</title><link>https://foosoft.net/posts/generating-the-foosoft.net-homepage/index.html</link><description>Exploring the uniquely custom process of generating this website with Goldsmith.</description><content:encoded><![CDATA[<p>The first iterations of FooSoft.net go back to the early 2000s, back when I was still in school. Back then, the site was entirely hand-written (proudly in Notepad!) and relied extensively on <a href="https://en.wikipedia.org/wiki/Frame_(World_Wide_Web)">HTML framesets</a> to reduce boilerplate code. Over the years, as frames fell out of favor due to issues with search engines and bookmarks, I had to rethink this approach.</p>
<p>This website has been rebuilt more times than I care to count. The stack evolved from using <a href="https://wordpress.com/">WordPress</a>, to <a href="https://www.concretecms.com/">ConcreteCMS</a>, to <a href="https://getpelican.com/">Pelican</a>, to <a href="https://hexo.io/">Hexo</a>, to <a href="https://gohugo.io/">Hugo</a> before briefly pausing on <a href="https://metalsmith.io/">Metalsmith</a>. Static site generation seemed to be the way to go (and yes, of course my WordPress site was hacked) for simplicity and portability. One problem I ran into was that most of the static site generators placed seemingly arbitrary restrictions on site structure. To enumerate a few of the issues:</p>
<ul>
<li>Text content has to live in a different directory from linked assets such as images and downloads.</li>
<li>Blog posts can be tagged with keywords but ordinary content pages cannot be.</li>
<li>Differences in implementation for blog posts and content pages make templating difficult.</li>
<li>Standalone HTML content (such as <a href="https://revealjs.com/">reveal.js</a> presentations) was broken by the generator.</li>
<li>Bending the generator to work with your desired directory layout led to complexity and pain.</li>
<li>Content generation was slow or non-reproducible between rebuilds.</li>
</ul>
<p>Out of all the static site generators I evaluated, Metalsmith came the closest to what I needed. I could finally begin to see a path forward to a homepage structured the way I wanted. Unfortunately, Metalsmith was quite temperamental due to shortcuts added in the pipeline design in what I can only assume to be an attempt to make the generator easier to use. It became clear to me that I needed to build my own tooling to get the results I could be truly satisfied with. <a href="https://git.sr.ht/~foosoft/goldsmith">Goldsmith</a>, my Go-based static generator, was born in late 2015.</p>
<h2 id="goals">Goals</h2>
<p>Here is a sampling of some hard requirements I relied on to guide me through implementation:</p>
<ul>
<li>Flexibility is paramount; must be able to achieve any design through customization.</li>
<li>All capabilities must be implemented through pipeline plugins to improve flexibility.</li>
<li>Page generation must be possible for in-place Git project repositories (GitHub mirror).</li>
<li>RSS feeds should be automatically generated for any page that needs publishing.</li>
<li>Post-process resources; generate thumbnails, minify HTML, CSS, and JavaScript.</li>
<li>Speed up iteration by immediately reflecting source changes on the generated site.</li>
<li>Arbitrary metadata must be supported for tagging, categorization, formatting, and more.</li>
</ul>
<h2 id="implementation">Implementation</h2>
<p>Let&rsquo;s just dive into the code. Many details have been omitted for the sake of brevity; let&rsquo;s just focus on flow for now:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#ff7b72">func</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">build</span>(contentDir,<span style="color:#6e7681"> </span>buildDir,<span style="color:#6e7681"> </span>cacheDir<span style="color:#6e7681"> </span><span style="color:#ff7b72">string</span>,<span style="color:#6e7681"> </span>devMode<span style="color:#6e7681"> </span><span style="color:#ff7b72">bool</span>)<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#8b949e;font-style:italic">// ...</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>errs<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>goldsmith.<span style="color:#d2a8ff;font-weight:bold">Begin</span>(contentDir).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Cache</span>(cacheDir).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Clean</span>(<span style="color:#79c0ff">true</span>).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(rule.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(new(indexRenamer)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(frontmatter.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(markdown.<span style="color:#d2a8ff;font-weight:bold">NewWithGoldmark</span>(gm)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">FilterPush</span>(condition.<span style="color:#d2a8ff;font-weight:bold">New</span>(!devMode)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(absolute.<span style="color:#d2a8ff;font-weight:bold">New</span>().<span style="color:#d2a8ff;font-weight:bold">BaseUrl</span>(baseUrl)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(minify.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">FilterPop</span>().<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(syndicate.<span style="color:#d2a8ff;font-weight:bold">New</span>(baseUrl,<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;Feed&#34;</span>).<span style="color:#d2a8ff;font-weight:bold">WithFeed</span>(<span style="color:#a5d6ff">&#34;posts&#34;</span>,<span style="color:#6e7681"> </span>feedConfig)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(collection.<span style="color:#d2a8ff;font-weight:bold">New</span>().<span style="color:#d2a8ff;font-weight:bold">Comparer</span>(collectionComparer)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">FilterPush</span>(operator.<span style="color:#d2a8ff;font-weight:bold">Not</span>(wildcard.<span style="color:#d2a8ff;font-weight:bold">New</span>(<span style="color:#a5d6ff">&#34;**/*.gohtml&#34;</span>))).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(index.<span style="color:#d2a8ff;font-weight:bold">New</span>(indexMeta)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">FilterPop</span>().<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(tags.<span style="color:#d2a8ff;font-weight:bold">New</span>().<span style="color:#d2a8ff;font-weight:bold">IndexMeta</span>(tagMeta)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(forward.<span style="color:#d2a8ff;font-weight:bold">New</span>(forwardMeta).<span style="color:#d2a8ff;font-weight:bold">PathMap</span>(forwardPathMap)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(layout.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(thumbnail.<span style="color:#d2a8ff;font-weight:bold">New</span>().<span style="color:#d2a8ff;font-weight:bold">Style</span>(thumbnail.Crop)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">FilterPush</span>(condition.<span style="color:#d2a8ff;font-weight:bold">New</span>(devMode)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(livejs.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">FilterPop</span>().<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">FilterPush</span>(condition.<span style="color:#d2a8ff;font-weight:bold">New</span>(!devMode)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(minify.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">FilterPop</span>().<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">End</span>(buildDir)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">for</span><span style="color:#6e7681"> </span>_,<span style="color:#6e7681"> </span>err<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">range</span><span style="color:#6e7681"> </span>errs<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>log.<span style="color:#d2a8ff;font-weight:bold">Print</span>(err)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre><h3 id="walkthrough">Walkthrough</h3>
<ol>
<li>
<p>Recursively scan all the files in the <code>contentDir</code> and start pushing them through the pipeline:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>goldsmith.<span style="color:#d2a8ff;font-weight:bold">Begin</span>(contentDir).<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Enable caching in <code>cacheDir</code> for supported plugins; this helps avoid reprocessing of unchanged resources:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Cache</span>(cacheDir).<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Enable cleanup of extra files found in the <code>buildDir</code> that are not build output:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Clean</span>(<span style="color:#79c0ff">true</span>).<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/rule">rule plugin</a> to enable per-directory rules, which are specified in a <code>rules.toml</code> file:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(rule.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span></code></pre><p>Rules can skip files matching a set of patterns. To prevent <code>.git</code> directories from being published, we can use the <code>drop</code> directive:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>[[drop]]
</span></span><span style="display:flex;"><span>accept = [
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;**/.git/**&#34;</span>,
</span></span><span style="display:flex;"><span>]
</span></span></code></pre><p>Rules can also help specify metadata on files that we do not want to modify directly (such as the <code>README.md</code> file for a project). We can instead match the file and apply properties to it externally:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>[[apply]]
</span></span><span style="display:flex;"><span>accept = [<span style="color:#a5d6ff">&#34;goldsmith/README.md&#34;</span>]
</span></span><span style="display:flex;"><span>props.Collection = <span style="color:#a5d6ff">&#34;ProjectsActive&#34;</span>
</span></span><span style="display:flex;"><span>props.Description = <span style="color:#a5d6ff">&#34;Static pipeline-based website generator written in Go.&#34;</span>
</span></span><span style="display:flex;"><span>props.GitHub = <span style="color:#a5d6ff">&#34;goldsmith&#34;</span>
</span></span><span style="display:flex;"><span>props.Tags = [<span style="color:#a5d6ff">&#34;generator&#34;</span>, <span style="color:#a5d6ff">&#34;golang&#34;</span>, <span style="color:#a5d6ff">&#34;goldsmith&#34;</span>, <span style="color:#a5d6ff">&#34;mit license&#34;</span>, <span style="color:#a5d6ff">&#34;web&#34;</span>]
</span></span></code></pre></li>
<li>
<p>Repositories on GitHub are expected to contain the project description inside of a <code>README.md</code> file. We want to rename all instances of <code>README.md</code> to <code>index.md</code> and ensure that each project directory will eventually contain an <code>index.html</code> file. I added a simple <code>indexRenamer</code> helper plugin to automate this:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(new(indexRenamer)).<span style="color:#6e7681">
</span></span></span></code></pre><p>The implementation of this one-off plugin is shown below:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#ff7b72">type</span><span style="color:#6e7681"> </span>indexRenamer<span style="color:#6e7681"> </span><span style="color:#ff7b72">struct</span>{}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">func</span><span style="color:#6e7681"> </span>(<span style="color:#ff7b72;font-weight:bold">*</span>indexRenamer)<span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">Name</span>()<span style="color:#6e7681"> </span><span style="color:#ff7b72">string</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;indexRenamer&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">func</span><span style="color:#6e7681"> </span>(<span style="color:#ff7b72;font-weight:bold">*</span>indexRenamer)<span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">Initialize</span>(context<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">*</span>goldsmith.Context)<span style="color:#6e7681"> </span><span style="color:#ff7b72">error</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>context.<span style="color:#d2a8ff;font-weight:bold">Filter</span>(wildcard.<span style="color:#d2a8ff;font-weight:bold">New</span>(<span style="color:#a5d6ff">&#34;**/README.md&#34;</span>))<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span><span style="color:#79c0ff">nil</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">func</span><span style="color:#6e7681"> </span>(<span style="color:#ff7b72;font-weight:bold">*</span>indexRenamer)<span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">Process</span>(context<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">*</span>goldsmith.Context,<span style="color:#6e7681"> </span>inputFile<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">*</span>goldsmith.File)<span style="color:#6e7681"> </span><span style="color:#ff7b72">error</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>dir<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>path.<span style="color:#d2a8ff;font-weight:bold">Dir</span>(inputFile.<span style="color:#d2a8ff;font-weight:bold">Path</span>())<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>inputFile.<span style="color:#d2a8ff;font-weight:bold">Rename</span>(path.<span style="color:#d2a8ff;font-weight:bold">Join</span>(dir,<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;index.md&#34;</span>))<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>context.<span style="color:#d2a8ff;font-weight:bold">DispatchFile</span>(inputFile)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span><span style="color:#79c0ff">nil</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/frontmatter">frontmatter plugin</a> to parse and strip out any metadata prefixed onto our files:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(frontmatter.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span></code></pre><p>This metadata consists of arbitrary properties and can be defined in several formats. The TOML frontmatter for this page is:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#f85149">+++</span>
</span></span><span style="display:flex;"><span>Date = <span style="color:#a5d6ff">2023-10-10</span>
</span></span><span style="display:flex;"><span>Description = <span style="color:#a5d6ff">&#34;Exploring the uniquely custom process of generating this website with Goldsmith.&#34;</span>
</span></span><span style="display:flex;"><span>Tags = [<span style="color:#a5d6ff">&#34;web&#34;</span>, <span style="color:#a5d6ff">&#34;goldsmith&#34;</span>, <span style="color:#a5d6ff">&#34;generator&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#f85149">+++</span>
</span></span></code></pre></li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/markdown">markdown plugin</a> to convert all Markdown files to HTML fragments:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(markdown.<span style="color:#d2a8ff;font-weight:bold">NewWithGoldmark</span>(gm)).<span style="color:#6e7681">
</span></span></span></code></pre><p>At this stage, these are not yet documents, but merely excerpts that must have header and footer markup templates applied to them before they become functional pages. This plugin uses the <a href="https://github.com/yuin/goldmark">goldmark</a> Markdown parser, which supports GitHub extensions, syntax highlighting and more. The desired features can be optionally configured by passing in a <code>goldmark</code> instance:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>gm<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>goldmark.<span style="color:#d2a8ff;font-weight:bold">New</span>(<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>goldmark.<span style="color:#d2a8ff;font-weight:bold">WithExtensions</span>(extension.GFM,<span style="color:#6e7681"> </span>extension.Typographer),<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>goldmark.<span style="color:#d2a8ff;font-weight:bold">WithParserOptions</span>(parser.<span style="color:#d2a8ff;font-weight:bold">WithAutoHeadingID</span>()),<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>goldmark.<span style="color:#d2a8ff;font-weight:bold">WithRendererOptions</span>(html.<span style="color:#d2a8ff;font-weight:bold">WithUnsafe</span>()),<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>goldmark.<span style="color:#d2a8ff;font-weight:bold">WithExtensions</span>(highlighting.<span style="color:#d2a8ff;font-weight:bold">NewHighlighting</span>(highlighting.<span style="color:#d2a8ff;font-weight:bold">WithStyle</span>(<span style="color:#a5d6ff">&#34;solarized-dark&#34;</span>))),<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>)<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Push a <a href="https://godoc.org/go.foosoft.net/goldsmith/filters/condition">condition filter</a> that ensures that pipeline manipulation within its scope only happens when the page is not being generated in <code>devMode</code> (preparing content for deployment to the web server):</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">FilterPush</span>(condition.<span style="color:#d2a8ff;font-weight:bold">New</span>(!self.dev)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(absolute.<span style="color:#d2a8ff;font-weight:bold">New</span>().<span style="color:#d2a8ff;font-weight:bold">BaseUrl</span>(baseUrl)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(minify.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">FilterPop</span>().<span style="color:#6e7681">
</span></span></span></code></pre><p>When generating the site for deployment, we chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/absolute">absolute plugin</a> to convert all relative URLs to absolute ones using the <code>baseUrl</code> (<a href="https://foosoft.net">https://foosoft.net</a>) prefix. This is required to ensure that all relative references are resolvable when the site&rsquo;s content is displayed in an RSS reader. We also apply the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/minify">minify plugin</a> to reduce the site&rsquo;s content to the minimal possible size without impacting behavior. We do not want to do this in development mode since it makes debugging the code a lot trickier! Once these two steps are completed, we pop off the condition filter and resume unconditional processing.</p>
</li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/syndicate">syndicate plugin</a> to generate feeds for selected articles (like this one):</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(syndicate.<span style="color:#d2a8ff;font-weight:bold">New</span>(baseUrl,<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;Feed&#34;</span>).<span style="color:#d2a8ff;font-weight:bold">WithFeed</span>(<span style="color:#a5d6ff">&#34;posts&#34;</span>,<span style="color:#6e7681"> </span>feedConfig)).<span style="color:#6e7681">
</span></span></span></code></pre><p>Only pages with the <code>Feed</code> metadata property with the value <code>posts</code> are considered during feed generation. We must also provide a basic site configuration and declare the desired feed formats.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>feedConfig<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>syndicate.FeedConfig{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>Title:<span style="color:#6e7681">       </span><span style="color:#a5d6ff">&#34;FooSoft Productions&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>Url:<span style="color:#6e7681">         </span>baseUrl,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>AuthorName:<span style="color:#6e7681">  </span><span style="color:#a5d6ff">&#34;Alex Yatskov&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>AuthorEmail:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;alex@foosoft.net&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>RssPath:<span style="color:#6e7681">     </span><span style="color:#a5d6ff">&#34;feeds/posts.xml&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>AtomPath:<span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;feeds/posts.atom&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>JsonPath:<span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;feeds/posts.json&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>ItemConfig:<span style="color:#6e7681"> </span>syndicate.ItemConfig{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>TitleKey:<span style="color:#6e7681">        </span><span style="color:#a5d6ff">&#34;Title&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>CreatedKey:<span style="color:#6e7681">      </span><span style="color:#a5d6ff">&#34;Date&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>DescriptionKey:<span style="color:#6e7681">  </span><span style="color:#a5d6ff">&#34;Description&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>ContentFromFile:<span style="color:#6e7681"> </span><span style="color:#79c0ff">true</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>},<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/collection">collection plugin</a> to group related pages into lists based on the value of the <code>Collection</code> metadata property:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(collection.<span style="color:#d2a8ff;font-weight:bold">New</span>().<span style="color:#d2a8ff;font-weight:bold">Comparer</span>(collectionComparer)).<span style="color:#6e7681">
</span></span></span></code></pre><p>The resulting collections are ordered using a custom comparer function which groups items first by <code>Date</code> and then <code>Title</code> properties:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>collectionComparer<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">func</span>(i,<span style="color:#6e7681"> </span>j<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">*</span>goldsmith.File)<span style="color:#6e7681"> </span><span style="color:#ff7b72">bool</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>iData,<span style="color:#6e7681"> </span>iOk<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>i.<span style="color:#d2a8ff;font-weight:bold">Prop</span>(<span style="color:#a5d6ff">&#34;Date&#34;</span>);<span style="color:#6e7681"> </span>iOk<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>jData,<span style="color:#6e7681"> </span>jOk<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>j.<span style="color:#d2a8ff;font-weight:bold">Prop</span>(<span style="color:#a5d6ff">&#34;Date&#34;</span>);<span style="color:#6e7681"> </span>jOk<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span>iData.(time.Time).<span style="color:#d2a8ff;font-weight:bold">UnixNano</span>()<span style="color:#6e7681"> </span>&gt;<span style="color:#6e7681"> </span>jData.(time.Time).<span style="color:#d2a8ff;font-weight:bold">UnixNano</span>()<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>iData,<span style="color:#6e7681"> </span>iOk<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>i.<span style="color:#d2a8ff;font-weight:bold">Prop</span>(<span style="color:#a5d6ff">&#34;Title&#34;</span>);<span style="color:#6e7681"> </span>iOk<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>jData,<span style="color:#6e7681"> </span>jOk<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>j.<span style="color:#d2a8ff;font-weight:bold">Prop</span>(<span style="color:#a5d6ff">&#34;Title&#34;</span>);<span style="color:#6e7681"> </span>jOk<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span>strings.<span style="color:#d2a8ff;font-weight:bold">Compare</span>(iData.(<span style="color:#ff7b72">string</span>),<span style="color:#6e7681"> </span>jData.(<span style="color:#ff7b72">string</span>))<span style="color:#6e7681"> </span>&lt;<span style="color:#6e7681"> </span><span style="color:#a5d6ff">0</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span>strings.<span style="color:#d2a8ff;font-weight:bold">Compare</span>(i.<span style="color:#d2a8ff;font-weight:bold">Path</span>(),<span style="color:#6e7681"> </span>j.<span style="color:#d2a8ff;font-weight:bold">Path</span>())<span style="color:#6e7681"> </span>&lt;<span style="color:#6e7681"> </span><span style="color:#a5d6ff">0</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Conditionally chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/index">index plugin</a> to generate directory index pages for paths that do not match the <code>**/*.gohtml</code> pattern:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">FilterPush</span>(operator.<span style="color:#d2a8ff;font-weight:bold">Not</span>(wildcard.<span style="color:#d2a8ff;font-weight:bold">New</span>(<span style="color:#a5d6ff">&#34;**/*.gohtml&#34;</span>))).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(index.<span style="color:#d2a8ff;font-weight:bold">New</span>(indexMeta)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">FilterPop</span>().<span style="color:#6e7681">
</span></span></span></code></pre><p>These template files are consumed later in the pipeline and we don&rsquo;t need to include them in the build output. Newly generated index pages are assigned the metadata configured in the plugin:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>indexMeta<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">map</span>[<span style="color:#ff7b72">string</span>]<span style="color:#ff7b72">any</span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;Layout&#34;</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;index&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/tags">tags plugin</a> to generate tag indices from page metadata:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(tags.<span style="color:#d2a8ff;font-weight:bold">New</span>().<span style="color:#d2a8ff;font-weight:bold">IndexMeta</span>(tagMeta)).<span style="color:#6e7681">
</span></span></span></code></pre><p>By default, this plugin builds lists based on the contents of the <code>Tags</code> property. Some additional configuration may be provided to specify what metadata is assigned to newly generated tag index pages.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>tagMeta<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">map</span>[<span style="color:#ff7b72">string</span>]<span style="color:#ff7b72">any</span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;Area&#34;</span>:<span style="color:#6e7681">   </span><span style="color:#a5d6ff">&#34;tags&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;Layout&#34;</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;tag&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/forward">forward plugin</a> to generate stub pages for content on the homepage that has been moved to a different URL:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(forward.<span style="color:#d2a8ff;font-weight:bold">New</span>(forwardMeta).<span style="color:#d2a8ff;font-weight:bold">PathMap</span>(forwardPathMap)).<span style="color:#6e7681">
</span></span></span></code></pre><p>We must specify the URL mapping as well as any additional metadata that should be assigned to newly generated forwarder pages:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>forwardMeta<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">map</span>[<span style="color:#ff7b72">string</span>]<span style="color:#ff7b72">any</span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;Layout&#34;</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;forward&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;Title&#34;</span>:<span style="color:#6e7681">  </span><span style="color:#a5d6ff">&#34;File Moved&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>forwardPathMap<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">map</span>[<span style="color:#ff7b72">string</span>]<span style="color:#ff7b72">string</span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;mangle/index.html&#34;</span>:<span style="color:#6e7681">                   </span><span style="color:#a5d6ff">&#34;/projects/mangle/&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;projects/argwrap/index.html&#34;</span>:<span style="color:#6e7681">         </span><span style="color:#a5d6ff">&#34;/projects/vim-argwrap/&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;projects/kanji-frequency/index.html&#34;</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;/posts/kanji-frequency/&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;projects/yomichan-chrome/index.html&#34;</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;/projects/yomichan/&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;yomichan/index.html&#34;</span>:<span style="color:#6e7681">                 </span><span style="color:#a5d6ff">&#34;/projects/yomichan/&#34;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/layout">layout plugin</a> to apply <a href="https://pkg.go.dev/html/template">Go HTML templates</a> to our page HTML fragments:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(layout.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span></code></pre><p>By default, the plugin searches for the <code>Layout</code> metadata property to decide which template to apply. All metadata associated with a file can be referenced from within these templates. The templates themselves are stored in <code>*.gohtml</code> files which are consumed by the pipeline along with all the other inputs. For example, the top-level template for the current page looks as follows:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>{{define<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;page&#34;</span>}}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>{{template<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;header&#34;</span><span style="color:#6e7681"> </span>.}}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>{{.Props.Content}}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>{{template<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;footer&#34;</span><span style="color:#6e7681"> </span>.}}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>{{end}}<span style="color:#6e7681">
</span></span></span></code></pre></li>
<li>
<p>Chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/thumbnail">thumbnail plugin</a> to automatically generate thumbnails for all processed image files:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(thumbnail.<span style="color:#d2a8ff;font-weight:bold">New</span>().<span style="color:#d2a8ff;font-weight:bold">Style</span>(thumbnail.Crop)).<span style="color:#6e7681">
</span></span></span></code></pre><p>By default, this produces PNG thumbnails resized to 128x128 pixels. The filename is automatically determined by appending <code>-thumb.png</code> to the source image filename after stripping the extension. For example, <code>foo.png</code> becomes <code>foo-thumb.png</code>.</p>
</li>
<li>
<p>Conditionally chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/livejs">livejs plugin</a> in development mode to inject JavaScript to refresh the page when a change is detected:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">FilterPush</span>(condition.<span style="color:#d2a8ff;font-weight:bold">New</span>(self.dev)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(livejs.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">FilterPop</span>().<span style="color:#6e7681">
</span></span></span></code></pre><p>This script continuously queries the server to detect changes. It should never be enabled on the homepage once it has been deployed.</p>
</li>
<li>
<p>Conditionally chain the <a href="https://godoc.org/go.foosoft.net/goldsmith/plugins/minify">minify plugin</a> once more to also minify the HTML templates we applied a couple of steps ago:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">FilterPush</span>(condition.<span style="color:#d2a8ff;font-weight:bold">New</span>(!self.dev)).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">Chain</span>(minify.<span style="color:#d2a8ff;font-weight:bold">New</span>()).<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">FilterPop</span>().<span style="color:#6e7681">
</span></span></span></code></pre><p>This plugin is chained twice so that both the RSS output (which is spun off earlier in the pipeline) and the full site are minified.</p>
</li>
<li>
<p>Finally, write out all the generated files to the <code>buildDir</code> directory.</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">End</span>(buildDir)<span style="color:#6e7681">
</span></span></span></code></pre><p>Today, over 400 files are generated by Goldsmith and output into the target directory. This process is highly parallelized and takes just over 100ms to complete with caching enabled and 130ms for a full rebuild. The results are fully deterministic, which makes inspecting content changes between website deployments easy.</p>
</li>
</ol>
<h3 id="error-handling">Error Handling</h3>
<p>If a plugin reports an error while processing a file, the file stops propagating through the pipeline. A list of errors is returned upon calling <code>End</code>. These errors contain the name of the file in question as well as the faulting plugin.</p>
<h2 id="directory-structure">Directory Structure</h2>
<p>One unique thing about the on-disk organization of FooSoft.net is that projects are actually included as submodules within the website repository. This makes it easy to keep the project&rsquo;s <code>README.md</code> up to date with the data shown on the generated homepage: it&rsquo;s literally the same file! I view this as an invaluable feature for keeping the project brand separate from where the project is hosted.</p>
<pre><code>├── build
├── cache
├── content
│   ├── 404.md
│   ├── CNAME
│   ├── css
│   │   └── main.css
│   ├── gohtml
│   │   ├── components.gohtml
│   │   └── content.gohtml
│   ├── go.mod
│   ├── img
│   │   ├── alex.png
│   │   └── brand.svg
│   ├── index.md
│   ├── js
│   │   └── main.js
│   ├── portfolio
│   │   ├── amaze
│   │   ├── arenanet
│   │   ├── index.md
│   │   ├── microsoft
│   │   └── rules.toml
│   ├── posts
│   │   ├── decrapifying-the-twitter-timeline
│   │   ├── fixing-firefox-video-playback-in-fedora-37
│   │   ├── generating-the-foosoft.net-homepage
│   │   ├── index.md
│   │   ├── installing-diablo-ii-on-linux
│   │   ├── kanji-frequency
│   │   ├── rules.toml
│   │   ├── saying-goodbye-to-vimscript
│   │   ├── squashing-all-commits-in-a-git-topic-branch
│   │   ├── sunsetting-the-yomichan-project
│   │   └── working-with-patches-in-git
│   ├── projects
│   │   ├── anki-connect
│   │   ├── ankijoy
│   │   ├── btac
│   │   ├── goldsmith
│   │   ├── goldsmith-components
│   │   ├── goldsmith-samples
│   │   ├── guid.nvim
│   │   ├── hlm2-wad-extract
│   │   ├── homemaker
│   │   ├── index.md
│   │   ├── jmdict
│   │   ├── lazarus
│   │   ├── mangle
│   │   ├── md2vim
│   │   ├── mdview
│   │   ├── meganekko
│   │   ├── metacall
│   │   ├── moonfall
│   │   ├── restaurant-search
│   │   ├── revolver.nvim
│   │   ├── rules.toml
│   │   ├── scrawl
│   │   ├── tetrys
│   │   ├── vfs
│   │   ├── vim-argwrap
│   │   ├── yomichan
│   │   ├── yomichan-anki
│   │   ├── yomichan-import
│   │   ├── zero-epwing
│   │   └── zero-epwing-go
│   ├── rules.toml
│   ├── tags
│   │   └── index.md
│   └── ttf
│       ├── MaterialIcons-Regular.ttf
│       ├── RobotoMono-Regular.ttf
│       ├── Roboto-Regular.ttf
│       └── RobotoSlab-Regular.ttf
├── deploy.sh
├── diff.sh
├── foosoft.github.io
├── go.mod
├── go.sum
├── main.go
├── rebuild.sh
├── serve.sh
└── update.sh
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>While this may seem like there are a lot of steps required to build this site, the actual cognitive load is very low. The statically generated output for FooSoft.net is a perfect match for my expectations and requires no hacks. Iteration is fast and adding new features is a breeze. If this seems interesting, take a look at the <a href="https://go.foosoft.net/goldsmith-samples">goldsmith-samples</a> repository for some simple examples to get started!</p>
]]></content:encoded><guid>https://foosoft.net/posts/generating-the-foosoft.net-homepage/index.html</guid><pubDate>Sun, 15 Oct 2023 00:00:00 -0700</pubDate></item><item><title>Passing the Torch to Yomitan</title><link>https://foosoft.net/posts/passing-the-torch-to-yomitan/index.html</link><description>A community Yomichan fork has reached a stable milestone release.</description><content:encoded><![CDATA[<p>Just over nine months ago, I officially announced the <a href="/posts/sunsetting-the-yomichan-project/">sunsetting</a> of the Yomichan project. For users, this meant that the extension would not receive any new updates, and would likely stop working with the approaching <a href="https://developer.chrome.com/docs/extensions/develop/migrate/mv2-deprecation-timeline">Manifest V2 deprecation</a>. Seeing as Yomichan has evolved to become a valuable tool in the Japanese learning community, I put out a request for community forks to keep this browser extension alive.</p>
<p>I decided against transferring extension ownership for two reasons. First, there was simply no way of knowing who would make any progress in completing the mandatory port to <a href="https://developer.chrome.com/docs/extensions/develop/migrate">Manifest V3</a>; what if they just squatted on the Yomichan name? Second, advertisers are happy to spend considerable capital (approaching five figures USD) to purchase extensions with established user bases (Yomichan has 100k+ users at the time of writing) and stuff them full of spyware. I still get weekly offers to sell out my users&hellip; I could never do this myself, but I&rsquo;m sure there are developers who would not hesitate.</p>
<p>Shortly after my announcement, <a href="https://github.com/themoeway">TheMoeWay</a>, a Japanese culture and learning community, reached out and expressed interest in maintaining a Yomichan fork. They have been professional in respecting my requests (establishing a distinct project identity, etc.) as well as maintaining proper attribution (unfortunately this has not been true of all forks). From what I&rsquo;ve observed lurking in <a href="https://discord.gg/nhqjydaR8j">TheMoeWay Discord</a>, there is a vibrant and active development community behind this new project (which even includes some former Yomichan contributors as well). After many months of effort, they have released a stable version of Yomitan. Crossing the Manifest V3 finish line is an amazing achievement by the group, and is why I now consider Yomitan to be the definitive successor to Yomichan.</p>
<p>Visit the <a href="https://github.com/themoeway/yomitan">Yomitan</a> project page for official Firefox and Chrome releases. TheMoeWay has also forked other supporting tools for dictionary generation and parsing; I view them as the natural evolution of the Yomichan toolchain. Finally, I am delighted that there are several other forks of Yomichan out there as well; having options is a great strength of open source development. That being said, projects should make sure to respect the licensing agreements and make proper attribution when ingesting code from other Yomichan derivatives.</p>
<p>Now that a stable alternative to Yomichan has emerged, Yomichan itself will be delisted from both Firefox and Chrome stores. Users who already have the extension installed should be able to continue using it until it is inevitably disabled by the browser for not supporting Manifest V3. A sincere thanks from me to all the users of Yomichan over the years!</p>
]]></content:encoded><guid>https://foosoft.net/posts/passing-the-torch-to-yomitan/index.html</guid><pubDate>Sun, 10 Dec 2023 00:00:00 -0700</pubDate></item><item><title>Leaving GitHub</title><link>https://foosoft.net/posts/leaving-github/index.html</link><description>After catching my fair share of stars, I'm leaving GitHub for a self-hosted solution.</description><content:encoded><![CDATA[<p>After more than a decade of open source development on <a href="https://github.com/FooSoft">GitHub</a> (my account dates back to January of 2011), I&rsquo;m parting ways with the service in favor of a self-hosted solution using Gitea. All of my private repositories have been deleted and the public ones archived. This should help ensure that existing code dependencies will remain intact, while making it clear that new development will not be taking place on GitHub.</p>
<p>I think most would agree that GitHub was a social network of sorts from its outset. This by itself was not an issue. Social networks back then (and other &ldquo;big web&rdquo; sites in general) were more focused on providing user value in order to encourage platform adoption. Over time, as these platforms attained monopolies in their respective domains, user value took a back seat to attaining business goals. This phenomenon, called <a href="https://en.wikipedia.org/wiki/Enshittification">enshittification</a> by Cory Doctorow, is described as follows:</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. I call this enshittification, and it is a seemingly inevitable consequence arising from the combination of the ease of changing how a platform allocates value, combined with the nature of a &ldquo;two sided market&rdquo;, where a platform sits between buyers and sellers, hold each hostage to the other, raking off an ever-larger share of the value that passes between them.</p>
</blockquote>
<p>Unfortunately, while still providing user value, GitHub has advanced significantly on the enshittification curve. There have been many examples of this over the past several years, such as:</p>
<ul>
<li>Forced algorithmic repository recommendations, many of which cannot be disabled.</li>
<li>Inline advertisements: &ldquo;Did you know you can ask your organization to pay for Copilot?&rdquo;</li>
<li>Arbitrary policy changes to require 2FA before letting me access my own repositories.</li>
<li>Training AI for current and future <a href="https://githubnext.com/">paid products</a> on my code with no way to opt out.</li>
</ul>
<p>In short, I can no longer say that the motivations of this platform align with my goals for continued FOSS development. I remember coming to this same conclusion when I quit Facebook many years ago. What was initially a useful site for keeping in contact with your friends and family devolved into a platform for advertisement and manipulation. In this light, keeping my repositories on GitHub has all the appeal of storing my personal photo albums on Facebook.</p>
<p>I fully expect that as a part of this departure, my projects will see diminished exposure to new users and contributors. This, again, is similar to the decrease in content impressions one experiences upon leaving Facebook. Fortunately, I don&rsquo;t really care about software development as a popularity contest anymore. The way I look at it now is that the applications I develop are designed with myself as the primary user. These applications also just happen to be open source to make them more reusable for others that choose to do so. I&rsquo;m no longer interested in intentionally increasing the user base or hunting for GitHub stars. Amusingly, I&rsquo;ve never noticed any correlation between the projects that were popular and the projects that I actually enjoyed working on.</p>
]]></content:encoded><guid>https://foosoft.net/posts/leaving-github/index.html</guid><pubDate>Mon, 01 Jan 2024 00:00:00 -0700</pubDate></item><item><title>Ongoing Changes to FooSoft.net</title><link>https://foosoft.net/posts/ongoing-changes-to-foosoft.net/index.html</link><description>Overview of some breaking changes to FooSoft.net.</description><content:encoded><![CDATA[<p>Recently, I&rsquo;ve been rethinking the role this website serves as my personal brand. Through the end of last year, I noticed a slowdown in my open source contributions. The satisfaction I receive from the programming was unchanged, but the support burden was taking a big toll on the enjoyment I was getting from engaging with my own projects. This has led me to reconsider the way I think about my projects; they are now more an artifact resulting from my hobby as opposed to a product for external consumption.</p>
<p>Adopting this model of focusing on code instead of stars and visibility has been extremely reinvigorating. I still value high-quality code contributions, but only to the extent that they improve use cases which are relevant to me. As part of this new outlook, I&rsquo;m making several changes to my homepage as well.</p>
<h2 id="homepage">Homepage</h2>
<p>For many years, the primary purpose of this website was to host information about the projects I&rsquo;m currently working on. Over time, this became increasingly redundant as literally all forges support rendering of the <code>README.md</code> file to help explain what a given repository is all about. Nevertheless, I have been maintaining an intricate static generation pipeline which would generate pages based on Git submodules hosted in the site&rsquo;s <code>projects</code> subdirectory. I&rsquo;m now doing away with all of this, not because it&rsquo;s difficult to maintain, but more because it&rsquo;s unnecessary complexity which doesn&rsquo;t add anything of value to the site.</p>
<p>After removing projects, I&rsquo;m left with an outdated website design which can be significantly trimmed down to essentially the blog and portfolio sections. I haven&rsquo;t yet figured out a new visual design, but it&rsquo;s going to be rather minimalist. I&rsquo;ve already started making structural changes in that now there are only two sections: the blog and the about page (which also serves as the portfolio). I&rsquo;ve been adding a lot of HTTP 301 redirects in my nginx configuration to prevent broken links where possible.</p>
<h2 id="repositories">Repositories</h2>
<p>At the start of last year, I <a href="/posts/leaving-github/">migrated all my projects off GitHub</a> to a self-hosted <a href="https://about.gitea.com/">Gitea</a> instance. After overcoming the initial FOMO of missed user engagement, I&rsquo;ve instead discovered Zen in the email-based patch workflow and the utter lack of caring about project popularity. The lack of other social features and Copilot upsells has been another benefit which cannot be overstated.</p>
<p>Alas, all good things must come to an end, and within the past couple of weeks, my Gitea instance has been getting completely destroyed by AI content scraping bots, the majority of which completely ignore the <code>robots.txt</code> file telling them to stay out. My choices were playing cat and mouse with the scrapers or moving back to a public forge. As the former was a lot more work than the latter, I got to searching for a new home for my projects.</p>
<p>The first forge I evaluated was <a href="https://codeberg.org/">Codeberg</a>, an up-and-coming GitHub alternative which is running <a href="https://forgejo.org/">Forgejo</a>, a fork of Gitea, the software I was previously self-hosting. I got as far as moving several projects over, but then a couple of days later Codeberg decided <a href="https://blog.codeberg.org/we-stay-strong-against-hate-and-hatred.html">to go political</a> over a two-word remark by some juvenile spammer, blowing up everything into yet another left versus right conflict.</p>
<blockquote>
<p>We will not be discouraged in our fight against far-right ideologies. They are currently on the rise in many parts of the world, and we believe it is important to protect all kinds of marginalized groups. However, if you believe this does not affect your project, you are wrong. Far-right forces pose a threat to all of us.</p>
</blockquote>
<p>As I do not want to associate my work with politics, signaling, or activism of any kind, it was time to move on. I eventually landed on <a href="https://sourcehut.org/">SourceHut</a>. While I can&rsquo;t say the user interface is very intuitive, I do like that this forge supports a patch-based workflow and clearly advertises itself as a paid service. It seems to support everything I need, without turning into another social network. I&rsquo;ve updated the redirects on my nginx instance so that old links to <code>git.foosoft.net</code> should now redirect to SourceHut (for both viewing project information and cloning over HTTPS). Sorry if I missed something and broke your workflow!</p>
<p>Finally, I&rsquo;ve delisted a lot of projects that are no longer maintained, and for all practical purposes are dead. I obviously still have copies of the repositories on my home server and can re-add anything if need be.</p>
]]></content:encoded><guid>https://foosoft.net/posts/ongoing-changes-to-foosoft.net/index.html</guid><pubDate>Sun, 16 Feb 2025 00:00:00 -0700</pubDate></item><item><title>Fixing BeamNG.drive on Flatpak Steam</title><link>https://foosoft.net/posts/fixing-beamng-drive-on-flatpak-steam/index.html</link><description/><content:encoded><![CDATA[<p>I run BeamNG.drive&rsquo;s native Linux build through the Flatpak version of Steam (on Arch, btw). It used to be stable, but after a system update it started crashing on launch more often than not.</p>
<p>Whenever the game fell over, <code>systemd-coredump</code> would show me something like this:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span>Stack trace of thread 12147:
</span></span><span style="display:flex;"><span>#0  0x00007fc6975cdfb5 n/a (libcef.so + 0x7fcdfb5)
</span></span><span style="display:flex;"><span>#1  0x00007fc6975ce380 n/a (libcef.so + 0x7fce380)
</span></span><span style="display:flex;"><span>#2  0x00007fc697f4eae7 n/a (libcef.so + 0x894eae7)
</span></span><span style="display:flex;"><span>#3  0x00007fc697f4c160 n/a (libcef.so + 0x894c160)
</span></span><span style="display:flex;"><span>#4  0x00007fc68cece332 n/a (/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.19.17 + 0x30332)
</span></span></code></pre><p>The game was stable when I launched <code>BinLinux/BeamNG.drive.x64</code> directly. Disabling the Steam Overlay did not help, so I started comparing environment variables. The important difference turned out to be one set by Flatpak Steam:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#79c0ff">LD_AUDIT</span><span style="color:#ff7b72;font-weight:bold">=</span>/app/links/<span style="color:#79c0ff">$LIB</span>/libshared-library-guard.so
</span></span></code></pre><p>Fortunately, the workaround is simple. Set the following launch option for BeamNG.drive in Steam:</p>
<pre style="color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none;"><code><span style="display:flex;"><span><span style="color:#79c0ff">LD_AUDIT</span><span style="color:#ff7b72;font-weight:bold">=</span> %command%
</span></span></code></pre><p>Flatpak Steam uses the dynamic linker&rsquo;s <code>LD_AUDIT</code> mechanism to load <code>libshared-library-guard.so</code>. Its job is to stop games from loading bundled libraries that are known to conflict with newer versions provided by Steam or the system. Clearing <code>LD_AUDIT</code> keeps the guard from loading, which stopped the crashes for me.</p>
<p>The exact cause remains unknown. It could involve an interaction with the dynamic linker or simply the timing of Chromium Embedded Framework initialization. Since the workaround is narrow in scope, I will leave further investigation as an exercise for the reader.</p>
]]></content:encoded><guid>https://foosoft.net/posts/fixing-beamng-drive-on-flatpak-steam/index.html</guid><pubDate>Wed, 02 Sep 2026 00:00:00 -0700</pubDate></item></channel></rss>