<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0"><channel><title>Forrest Jacobs</title><link>https://forrestjacobs.com/</link><description>Forrest Jacobs’s personal technical blog.</description><generator>Hugo 0.152.2</generator><lastBuildDate>Fri, 29 Aug 2025 20:42:00 -0400</lastBuildDate><item><title>Shell-agnostic config</title><pubDate>Fri, 29 Aug 2025 20:42:00 -0400</pubDate><link>https://forrestjacobs.com/shell-agnostic-config/</link><guid>https://forrestjacobs.com/shell-agnostic-config/</guid><description>&lt;p&gt;I love &lt;a href="https://fishshell.com/"&gt;fish&lt;/a&gt;, and I have built up my fish config over time:&lt;/p&gt;
&lt;figure class="codeblock"&gt;
&lt;figcaption class="name"&gt;~/.config/fish/config.fish&lt;/figcaption&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 17.0625rem;"
&gt;&lt;code class="language-fish"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Set helix as the editor if it&amp;#39;s available.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="na"&gt;-q&lt;/span&gt; hx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="na"&gt;-gx&lt;/span&gt; &lt;span class="nv"&gt;EDITOR&lt;/span&gt; hx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="na"&gt;-gx&lt;/span&gt; &lt;span class="nv"&gt;MANOPT&lt;/span&gt; &lt;span class="na"&gt;--no-justification&lt;/span&gt; &lt;span class="c"&gt;# Badly justified text is terrible
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;abbr &lt;/span&gt;S &lt;span class="s1"&gt;&amp;#39;sudo systemctl&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# etc...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I’ve started working in environments where it’s infeasible to switch the shell from bash, but I still want to use my config. I &lt;em&gt;could&lt;/em&gt; make a parallel &lt;code&gt;.bashrc&lt;/code&gt; with the equivalent commands, but that’s bound to get out of date. Instead, I turned my fish config into a bash script that prints out the correct instructions for either bash or fish (a la &lt;a href="https://docs.brew.sh/Manpage#shellenv-shell-"&gt;&lt;code&gt;brew shellenv [shell]&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://www.mankier.com/1/zoxide-init"&gt;&lt;code&gt;zoxide init [shell]&lt;/code&gt;&lt;/a&gt;). It looks like this:&lt;/p&gt;
&lt;figure class="codeblock"&gt;
&lt;figcaption class="name"&gt;init_shell.sh&lt;/figcaption&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 77.1875rem;"
&gt;&lt;code class="language-fish"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="ch"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="ch"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# init_shell: Prints instructions to configure the given shell.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Add `eval &amp;#34;$(init_shell bash)&amp;#34;` to .bashrc to set up bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# or `init_shell fish | source` to config.fish to set up fish.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Escapes and prints the passed in string.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;local&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;${&lt;/span&gt;text//&lt;span class="se"&gt;\\&lt;/span&gt;/&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;${&lt;/span&gt;text//&lt;span class="se"&gt;\&amp;#39;&lt;/span&gt;/&lt;span class="se"&gt;\\\&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Prints a command that sets an environment variable in $shell.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Pass in the variable name, then the value.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&lt;/span&gt;&lt;span class="nf"&gt;p_export&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;export&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;${&lt;/span&gt;&lt;span class="s2"&gt;shell}&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;fish&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;set -xg &lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;escape &amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;) &lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;escape &amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;export &lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;escape &amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;)=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;escape &amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Prints a command that sets an abbreviation in $shell.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Pass in the abbreviation name, then the full command.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&lt;/span&gt;&lt;span class="nf"&gt;p_abbr&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;${&lt;/span&gt;&lt;span class="s2"&gt;shell}&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;fish&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;abbr &lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;escape &amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;) &lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;escape &amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;alias &lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;escape &amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;)=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;escape &amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Set helix as the editor if it&amp;#39;s available.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="na"&gt;-v&lt;/span&gt; hx &lt;span class="o"&gt;&amp;amp;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;p_export&lt;/span&gt; EDITOR hx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;p_export&lt;/span&gt; MANOPT &lt;span class="na"&gt;--no-justification&lt;/span&gt; &lt;span class="c"&gt;# Badly justified text is terrible
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;p_abbr&lt;/span&gt; S &lt;span class="s1"&gt;&amp;#39;sudo systemctl&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# etc...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Admittedly, this design introduces quite a bit of complexity. For example, you&amp;rsquo;ll notice the &lt;code&gt;p_export&lt;/code&gt; function exports the environment variable in the script before printing out the export command. This is to ensure that later commands in this script can access the environment variable the same way the shell would. You can imagine how easily the command&amp;rsquo;s internal state could drift from the shell&amp;rsquo;s state in subtle, difficult to debug ways.&lt;/p&gt;
&lt;p&gt;But this additional complexity is worth it for me. My other options are to abandon or duplicate my config (yuck), or switch to bash everywhere (double-yuck). This design has been working well for me for a few weeks.&lt;/p&gt;</description></item><item><title>Rust to Go and back</title><pubDate>Mon, 19 May 2025 13:11:00 -0400</pubDate><link>https://forrestjacobs.com/rust-to-go-and-back/</link><guid>https://forrestjacobs.com/rust-to-go-and-back/</guid><description>&lt;p&gt;I wrote two Discord bots relatively recently: &lt;a href="https://github.com/forrestjacobs/systemctl-bot"&gt;a bot called &lt;code&gt;systemctl-bot&lt;/code&gt; that lets you start and stop systemd units&lt;/a&gt;, and &lt;a href="https://github.com/forrestjacobs/pipe-bot"&gt;a bot called &lt;code&gt;pipe-bot&lt;/code&gt; that posts piped messages&lt;/a&gt;. I attempted to write both in Rust, and while one was a delight to write, the other I ended up rewriting in Go in a fit of frustration. Here are a few reasons why:&lt;/p&gt;
&lt;h2 id="starting-off-too-complex"&gt;Starting off too complex&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pipe-bot&lt;/code&gt;, the program I successfully wrote in Rust, is very simple — it listens to standard in, then calls to the Discord API based on the message:&lt;/p&gt;
&lt;figure&gt;

&lt;div class="goat"&gt;
 
 &lt;svg
 xmlns="http://www.w3.org/2000/svg"
 viewBox="0 0 576 393"
 aria-label="Diagram showing the flow of execution for pipe-bot."
 &gt;
 &lt;g transform='translate(8,16)'&gt;
&lt;path d='M 200,16 L 368,16' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 200,48 L 280,48' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,48 L 368,48' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 0,80 L 16,80' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 72,80 L 272,80' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 288,80 L 560,80' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 208,112 L 360,112' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 192,144 L 280,144' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,144 L 344,144' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 208,192 L 336,192' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 160,208 L 208,208' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 336,208 L 384,208' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 208,224 L 280,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,224 L 336,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,304 L 160,304' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 200,304 L 344,304' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 384,304 L 496,304' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,336 L 160,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 200,336 L 344,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 384,336 L 496,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 0,368 L 560,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 0,80 L 0,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,304 L 24,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 160,304 L 160,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 200,304 L 200,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 208,192 L 208,208' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 208,208 L 208,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,48 L 280,96' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,144 L 280,176' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,224 L 280,240' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,272 L 280,288' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 336,192 L 336,208' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 336,208 L 336,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 344,304 L 344,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 384,304 L 384,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 496,304 L 496,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 560,80 L 560,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 120,288 L 128,272' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 144,240 L 160,208' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 192,144 L 208,112' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 344,144 L 360,112' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 384,208 L 400,240' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 416,272 L 424,288' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 112,304 L 120,288' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='138.000000,288.000000 126.000000,282.399994 126.000000,293.600006' fill='currentColor' transform='rotate(120.000000, 120.000000, 288.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 280,96 L 280,104' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='296.000000,96.000000 284.000000,90.400002 284.000000,101.599998' fill='currentColor' transform='rotate(90.000000, 280.000000, 96.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 280,176 L 280,184' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='296.000000,176.000000 284.000000,170.399994 284.000000,181.600006' fill='currentColor' transform='rotate(90.000000, 280.000000, 176.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 280,288 L 280,296' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='296.000000,288.000000 284.000000,282.399994 284.000000,293.600006' fill='currentColor' transform='rotate(90.000000, 280.000000, 288.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 424,288 L 432,304' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='442.000000,288.000000 430.000000,282.399994 430.000000,293.600006' fill='currentColor' transform='rotate(60.000000, 424.000000, 288.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 200,16 A 16,16 0 0,0 184,32' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 368,16 A 16,16 0 0,1 384,32' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 184,32 A 16,16 0 0,0 200,48' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 384,32 A 16,16 0 0,1 368,48' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;text text-anchor='middle' x='32' y='84' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='40' y='84' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='48' y='84' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='48' y='324' fill='currentColor' style='font-size:1em'&gt;S&lt;/text&gt;
&lt;text text-anchor='middle' x='56' y='84' fill='currentColor' style='font-size:1em'&gt;p&lt;/text&gt;
&lt;text text-anchor='middle' x='56' y='324' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='64' y='324' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='72' y='324' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='88' y='324' fill='currentColor' style='font-size:1em'&gt;M&lt;/text&gt;
&lt;text text-anchor='middle' x='96' y='324' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='104' y='260' fill='currentColor' style='font-size:1em'&gt;M&lt;/text&gt;
&lt;text text-anchor='middle' x='104' y='324' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='112' y='260' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='112' y='324' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='120' y='260' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='120' y='324' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='128' y='260' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='128' y='324' fill='currentColor' style='font-size:1em'&gt;g&lt;/text&gt;
&lt;text text-anchor='middle' x='136' y='260' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='136' y='324' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='144' y='260' fill='currentColor' style='font-size:1em'&gt;g&lt;/text&gt;
&lt;text text-anchor='middle' x='152' y='260' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='208' y='36' fill='currentColor' style='font-size:1em'&gt;S&lt;/text&gt;
&lt;text text-anchor='middle' x='216' y='36' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='224' y='36' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='224' y='132' fill='currentColor' style='font-size:1em'&gt;W&lt;/text&gt;
&lt;text text-anchor='middle' x='224' y='260' fill='currentColor' style='font-size:1em'&gt;S&lt;/text&gt;
&lt;text text-anchor='middle' x='224' y='324' fill='currentColor' style='font-size:1em'&gt;U&lt;/text&gt;
&lt;text text-anchor='middle' x='232' y='36' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='232' y='132' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='232' y='212' fill='currentColor' style='font-size:1em'&gt;P&lt;/text&gt;
&lt;text text-anchor='middle' x='232' y='260' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='232' y='324' fill='currentColor' style='font-size:1em'&gt;p&lt;/text&gt;
&lt;text text-anchor='middle' x='240' y='36' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='240' y='132' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='240' y='212' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='240' y='260' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='240' y='324' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='248' y='132' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='248' y='212' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='248' y='260' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='248' y='324' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='256' y='36' fill='currentColor' style='font-size:1em'&gt;D&lt;/text&gt;
&lt;text text-anchor='middle' x='256' y='212' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='256' y='260' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='256' y='324' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='264' y='36' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='264' y='132' fill='currentColor' style='font-size:1em'&gt;f&lt;/text&gt;
&lt;text text-anchor='middle' x='264' y='212' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='264' y='260' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='264' y='324' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='272' y='36' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='272' y='132' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='280' y='36' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='280' y='132' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='280' y='212' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='280' y='260' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='280' y='324' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='288' y='36' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='288' y='212' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='288' y='260' fill='currentColor' style='font-size:1em'&gt;p&lt;/text&gt;
&lt;text text-anchor='middle' x='288' y='324' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='296' y='36' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='296' y='132' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='296' y='212' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='296' y='260' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='296' y='324' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='304' y='36' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='304' y='132' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='304' y='212' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='304' y='260' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='304' y='324' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='312' y='132' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='312' y='212' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='312' y='260' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='312' y='324' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='320' y='36' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='320' y='132' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='320' y='260' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='320' y='324' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='328' y='36' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='328' y='132' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='336' y='36' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='344' y='36' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='352' y='36' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='360' y='36' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='392' y='260' fill='currentColor' style='font-size:1em'&gt;E&lt;/text&gt;
&lt;text text-anchor='middle' x='400' y='260' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='408' y='260' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='408' y='324' fill='currentColor' style='font-size:1em'&gt;L&lt;/text&gt;
&lt;text text-anchor='middle' x='416' y='260' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='416' y='324' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='424' y='324' fill='currentColor' style='font-size:1em'&gt;g&lt;/text&gt;
&lt;text text-anchor='middle' x='440' y='324' fill='currentColor' style='font-size:1em'&gt;E&lt;/text&gt;
&lt;text text-anchor='middle' x='448' y='324' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='456' y='324' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='464' y='324' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='472' y='324' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;/g&gt;

 &lt;/svg&gt;
 
&lt;/div&gt;
&lt;figcaption&gt;
A diagram showing the flow of execution for &lt;code&gt;pipe-bot&lt;/code&gt;. After starting a Discord client, it waits for stdin, parses it, and then either sends a message, updates the Discord status, or logs an error based on the parsed message.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;However, I started with &lt;code&gt;systemctl-bot&lt;/code&gt;, which monitors and controls systemd units, parses and shares a config file, reads async streams, and generally has weird edge cases. While it’s not &lt;em&gt;overly&lt;/em&gt; complex, it’s a lot to get your head around when you’re also learning the borrower checker and async Rust.&lt;/p&gt;
&lt;figure&gt;

&lt;div class="goat"&gt;
 
 &lt;svg
 xmlns="http://www.w3.org/2000/svg"
 viewBox="0 0 616 633"
 aria-label="Diagram showing the flow of execution for systemctl-bot."
 &gt;
 &lt;g transform='translate(8,16)'&gt;
&lt;path d='M 264,0 L 368,0' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 264,32 L 320,32' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 320,32 L 368,32' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 232,80 L 400,80' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 160,96 L 216,96' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 232,112 L 320,112' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 320,112 L 400,112' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 248,160 L 392,160' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 408,176 L 416,176' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 248,192 L 392,192' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 0,224 L 16,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 128,224 L 136,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 152,224 L 256,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,224 L 296,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 416,224 L 424,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 440,224 L 600,224' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 40,256 L 224,256' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 392,256 L 488,256' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,288 L 144,288' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 144,288 L 208,288' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 376,288 L 472,288' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,336 L 232,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 352,336 L 504,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,368 L 144,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 144,368 L 232,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 352,400 L 376,400' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 376,400 L 480,400' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 480,400 L 504,400' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,416 L 232,416' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,448 L 232,448' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 0,480 L 256,480' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 296,480 L 408,480' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 432,480 L 584,480' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 296,512 L 408,512' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 432,512 L 504,512' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 504,512 L 584,512' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 440,560 L 576,560' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 440,592 L 576,592' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,608 L 600,608' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 0,224 L 0,480' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,336 L 24,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,416 L 24,448' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 144,112 L 144,240' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 144,288 L 144,320' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 144,368 L 144,400' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 232,336 L 232,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 232,416 L 232,448' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 256,224 L 256,480' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 280,224 L 280,608' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 296,480 L 296,512' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 320,32 L 320,64' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 320,112 L 320,144' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 408,480 L 408,512' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 432,192 L 432,240' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 432,304 L 432,320' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 432,480 L 432,512' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 440,560 L 440,592' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 504,512 L 504,544' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 576,560 L 576,592' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 584,480 L 584,512' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 600,224 L 600,608' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 24,288 L 40,256' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 208,288 L 224,256' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 336,368 L 352,336' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 376,288 L 392,256' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 344,464 L 352,448' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 368,416 L 376,400' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 472,288 L 488,256' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 504,400 L 520,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 336,368 L 352,400' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 480,400 L 488,416' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 504,448 L 512,464' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 504,336 L 520,368' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 432,296 L 432,304' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 144,240 L 144,248' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='160.000000,240.000000 148.000000,234.399994 148.000000,245.600006' fill='currentColor' transform='rotate(90.000000, 144.000000, 240.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 144,320 L 144,328' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='160.000000,320.000000 148.000000,314.399994 148.000000,325.600006' fill='currentColor' transform='rotate(90.000000, 144.000000, 320.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 144,400 L 144,408' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='160.000000,400.000000 148.000000,394.399994 148.000000,405.600006' fill='currentColor' transform='rotate(90.000000, 144.000000, 400.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 320,64 L 320,72' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='336.000000,64.000000 324.000000,58.400002 324.000000,69.599998' fill='currentColor' transform='rotate(90.000000, 320.000000, 64.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 320,144 L 320,152' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='336.000000,144.000000 324.000000,138.399994 324.000000,149.600006' fill='currentColor' transform='rotate(90.000000, 320.000000, 144.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 336,480 L 344,464' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='362.000000,464.000000 350.000000,458.399994 350.000000,469.600006' fill='currentColor' transform='rotate(120.000000, 344.000000, 464.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 432,240 L 432,248' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='448.000000,240.000000 436.000000,234.399994 436.000000,245.600006' fill='currentColor' transform='rotate(90.000000, 432.000000, 240.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 432,320 L 432,328' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='448.000000,320.000000 436.000000,314.399994 436.000000,325.600006' fill='currentColor' transform='rotate(90.000000, 432.000000, 320.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 504,544 L 504,552' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='520.000000,544.000000 508.000000,538.400024 508.000000,549.599976' fill='currentColor' transform='rotate(90.000000, 504.000000, 544.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 512,464 L 520,480' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;polygon points='530.000000,464.000000 518.000000,458.399994 518.000000,469.600006' fill='currentColor' transform='rotate(60.000000, 512.000000, 464.000000)'&gt;&lt;/polygon&gt;
&lt;path d='M 264,0 A 16,16 0 0,0 248,16' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 368,0 A 16,16 0 0,1 384,16' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 248,16 A 16,16 0 0,0 264,32' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 384,16 A 16,16 0 0,1 368,32' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 232,80 A 16,16 0 0,0 216,96' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 400,80 A 16,16 0 0,1 416,96' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 160,96 A 16,16 0 0,0 144,112' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 216,96 A 16,16 0 0,0 232,112' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 416,96 A 16,16 0 0,1 400,112' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 248,160 A 16,16 0 0,0 232,176' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 392,160 A 16,16 0 0,1 408,176' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 416,176 A 16,16 0 0,1 432,192' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 232,176 A 16,16 0 0,0 248,192' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;path d='M 408,176 A 16,16 0 0,1 392,192' fill='none' stroke='currentColor'&gt;&lt;/path&gt;
&lt;text text-anchor='middle' x='32' y='228' fill='currentColor' style='font-size:1em'&gt;S&lt;/text&gt;
&lt;text text-anchor='middle' x='40' y='228' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='48' y='228' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='48' y='356' fill='currentColor' style='font-size:1em'&gt;F&lt;/text&gt;
&lt;text text-anchor='middle' x='48' y='436' fill='currentColor' style='font-size:1em'&gt;U&lt;/text&gt;
&lt;text text-anchor='middle' x='56' y='228' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='56' y='276' fill='currentColor' style='font-size:1em'&gt;U&lt;/text&gt;
&lt;text text-anchor='middle' x='56' y='356' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='56' y='436' fill='currentColor' style='font-size:1em'&gt;p&lt;/text&gt;
&lt;text text-anchor='middle' x='64' y='228' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='64' y='276' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='64' y='356' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='64' y='436' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='72' y='228' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='72' y='276' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='72' y='356' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='72' y='436' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='80' y='276' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='80' y='356' fill='currentColor' style='font-size:1em'&gt;h&lt;/text&gt;
&lt;text text-anchor='middle' x='80' y='436' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='88' y='228' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='88' y='436' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='96' y='228' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='96' y='276' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='96' y='356' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='104' y='228' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='104' y='276' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='104' y='356' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='104' y='436' fill='currentColor' style='font-size:1em'&gt;D&lt;/text&gt;
&lt;text text-anchor='middle' x='112' y='228' fill='currentColor' style='font-size:1em'&gt;p&lt;/text&gt;
&lt;text text-anchor='middle' x='112' y='276' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='112' y='356' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='112' y='436' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='120' y='276' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='120' y='356' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='120' y='436' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='128' y='276' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='128' y='356' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='128' y='436' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='136' y='276' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='136' y='356' fill='currentColor' style='font-size:1em'&gt;'&lt;/text&gt;
&lt;text text-anchor='middle' x='136' y='436' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='144' y='436' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='152' y='276' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='152' y='356' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='152' y='436' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='160' y='276' fill='currentColor' style='font-size:1em'&gt;p&lt;/text&gt;
&lt;text text-anchor='middle' x='160' y='356' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='168' y='276' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='168' y='356' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='168' y='436' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='176' y='276' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='176' y='356' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='176' y='436' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='184' y='276' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='184' y='356' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='184' y='436' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='192' y='276' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='192' y='356' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='192' y='436' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='200' y='356' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='200' y='436' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='208' y='356' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='208' y='436' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='240' y='100' fill='currentColor' style='font-size:1em'&gt;S&lt;/text&gt;
&lt;text text-anchor='middle' x='248' y='100' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='256' y='100' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='256' y='180' fill='currentColor' style='font-size:1em'&gt;R&lt;/text&gt;
&lt;text text-anchor='middle' x='264' y='100' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='264' y='180' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='272' y='20' fill='currentColor' style='font-size:1em'&gt;P&lt;/text&gt;
&lt;text text-anchor='middle' x='272' y='100' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='272' y='180' fill='currentColor' style='font-size:1em'&gt;g&lt;/text&gt;
&lt;text text-anchor='middle' x='280' y='20' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='280' y='180' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='288' y='20' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='288' y='100' fill='currentColor' style='font-size:1em'&gt;D&lt;/text&gt;
&lt;text text-anchor='middle' x='288' y='180' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='296' y='20' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='296' y='100' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='296' y='180' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='304' y='20' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='304' y='100' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='304' y='180' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='312' y='100' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='312' y='180' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='312' y='228' fill='currentColor' style='font-size:1em'&gt;C&lt;/text&gt;
&lt;text text-anchor='middle' x='320' y='20' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='320' y='100' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='320' y='228' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='320' y='500' fill='currentColor' style='font-size:1em'&gt;L&lt;/text&gt;
&lt;text text-anchor='middle' x='328' y='20' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='328' y='100' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='328' y='180' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='328' y='228' fill='currentColor' style='font-size:1em'&gt;m&lt;/text&gt;
&lt;text text-anchor='middle' x='328' y='500' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='336' y='20' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='336' y='100' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='336' y='180' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='336' y='228' fill='currentColor' style='font-size:1em'&gt;m&lt;/text&gt;
&lt;text text-anchor='middle' x='336' y='500' fill='currentColor' style='font-size:1em'&gt;g&lt;/text&gt;
&lt;text text-anchor='middle' x='344' y='20' fill='currentColor' style='font-size:1em'&gt;f&lt;/text&gt;
&lt;text text-anchor='middle' x='344' y='180' fill='currentColor' style='font-size:1em'&gt;m&lt;/text&gt;
&lt;text text-anchor='middle' x='344' y='228' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='352' y='20' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='352' y='100' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='352' y='180' fill='currentColor' style='font-size:1em'&gt;m&lt;/text&gt;
&lt;text text-anchor='middle' x='352' y='228' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='352' y='500' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='360' y='20' fill='currentColor' style='font-size:1em'&gt;g&lt;/text&gt;
&lt;text text-anchor='middle' x='360' y='100' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='360' y='180' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='360' y='228' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='360' y='372' fill='currentColor' style='font-size:1em'&gt;I&lt;/text&gt;
&lt;text text-anchor='middle' x='360' y='436' fill='currentColor' style='font-size:1em'&gt;N&lt;/text&gt;
&lt;text text-anchor='middle' x='360' y='500' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='368' y='100' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='368' y='180' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='368' y='372' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='368' y='436' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='368' y='500' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='376' y='100' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='376' y='180' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='376' y='228' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='376' y='500' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='384' y='100' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='384' y='180' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='384' y='228' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='384' y='372' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='384' y='500' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='392' y='100' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='392' y='228' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='392' y='372' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='400' y='228' fill='currentColor' style='font-size:1em'&gt;p&lt;/text&gt;
&lt;text text-anchor='middle' x='400' y='372' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='408' y='276' fill='currentColor' style='font-size:1em'&gt;C&lt;/text&gt;
&lt;text text-anchor='middle' x='408' y='372' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='416' y='276' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='424' y='276' fill='currentColor' style='font-size:1em'&gt;m&lt;/text&gt;
&lt;text text-anchor='middle' x='424' y='372' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='432' y='276' fill='currentColor' style='font-size:1em'&gt;m&lt;/text&gt;
&lt;text text-anchor='middle' x='432' y='372' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='440' y='276' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='448' y='276' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='448' y='372' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='456' y='276' fill='currentColor' style='font-size:1em'&gt;d&lt;/text&gt;
&lt;text text-anchor='middle' x='456' y='372' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='456' y='500' fill='currentColor' style='font-size:1em'&gt;C&lt;/text&gt;
&lt;text text-anchor='middle' x='464' y='372' fill='currentColor' style='font-size:1em'&gt;n&lt;/text&gt;
&lt;text text-anchor='middle' x='464' y='500' fill='currentColor' style='font-size:1em'&gt;a&lt;/text&gt;
&lt;text text-anchor='middle' x='464' y='580' fill='currentColor' style='font-size:1em'&gt;P&lt;/text&gt;
&lt;text text-anchor='middle' x='472' y='372' fill='currentColor' style='font-size:1em'&gt;f&lt;/text&gt;
&lt;text text-anchor='middle' x='472' y='500' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='472' y='580' fill='currentColor' style='font-size:1em'&gt;o&lt;/text&gt;
&lt;text text-anchor='middle' x='480' y='372' fill='currentColor' style='font-size:1em'&gt;i&lt;/text&gt;
&lt;text text-anchor='middle' x='480' y='500' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='480' y='580' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='488' y='372' fill='currentColor' style='font-size:1em'&gt;g&lt;/text&gt;
&lt;text text-anchor='middle' x='488' y='436' fill='currentColor' style='font-size:1em'&gt;Y&lt;/text&gt;
&lt;text text-anchor='middle' x='488' y='580' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='496' y='372' fill='currentColor' style='font-size:1em'&gt;?&lt;/text&gt;
&lt;text text-anchor='middle' x='496' y='436' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='496' y='500' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='504' y='436' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='504' y='500' fill='currentColor' style='font-size:1em'&gt;y&lt;/text&gt;
&lt;text text-anchor='middle' x='504' y='580' fill='currentColor' style='font-size:1em'&gt;r&lt;/text&gt;
&lt;text text-anchor='middle' x='512' y='500' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='512' y='580' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='520' y='500' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='520' y='580' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='528' y='500' fill='currentColor' style='font-size:1em'&gt;e&lt;/text&gt;
&lt;text text-anchor='middle' x='528' y='580' fill='currentColor' style='font-size:1em'&gt;u&lt;/text&gt;
&lt;text text-anchor='middle' x='536' y='500' fill='currentColor' style='font-size:1em'&gt;m&lt;/text&gt;
&lt;text text-anchor='middle' x='536' y='580' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;text text-anchor='middle' x='544' y='500' fill='currentColor' style='font-size:1em'&gt;c&lt;/text&gt;
&lt;text text-anchor='middle' x='544' y='580' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='552' y='500' fill='currentColor' style='font-size:1em'&gt;t&lt;/text&gt;
&lt;text text-anchor='middle' x='552' y='580' fill='currentColor' style='font-size:1em'&gt;s&lt;/text&gt;
&lt;text text-anchor='middle' x='560' y='500' fill='currentColor' style='font-size:1em'&gt;l&lt;/text&gt;
&lt;/g&gt;

 &lt;/svg&gt;
 
&lt;/div&gt;
&lt;figcaption&gt;
A diagram showing the more complex flow of execution for &lt;code&gt;systemctl-bot&lt;/code&gt;. It parses a config file, starts a Discord client, and then branches off into two threads: one handling status updates, and the other handling commands (after registering them with the Discord client.) The status thread waits for unit status updates, then fetches all units' statuses and updates Discord. The command loop waits for a user command, calls &lt;code&gt;systemctl&lt;/code&gt; with the appropriate arguments (provided that the targeted unit is in the config file) and then posts the results. If the unit was not in the config file it logs an error instead.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="async-rust"&gt;Async Rust&lt;/h2&gt;
&lt;p&gt;I anticipated fighting with the borrower checker, but—oh boy!—it pales in comparison to writing and understanding async Rust. Since I was coming from the world of “””enterprise software”””, I was used to writing with a level of indirection to facilitate code reuse, unit testing, and refactoring. However, Rust makes you pay for indirection that involves tracking more state or more complex state since it has to track that state while the async call is in progress. Watch this video to hear &lt;a href="https://fasterthanli.me/"&gt;someone much smarter than me&lt;/a&gt; explain why the current state of async Rust ain’t quite it yet:&lt;/p&gt;


&lt;div class="youtube"&gt;
&lt;figure&gt;
&lt;figcaption&gt;&lt;a href="https://www.youtube.com/watch?v=bnmln9HtqEI"&gt;
&lt;span&gt;&lt;span class="title"&gt;Catching up with async Rust&lt;/span&gt; by fasterthanlime on YouTube&lt;/span&gt;
&lt;/a&gt;&lt;/figcaption&gt;
&lt;img src="https://forrestjacobs.com/rust-to-go-and-back/bnmln9HtqEI.webp"
 style="max-width: 100%; width: auto; height: auto;"
 width="1280" height="720"
 alt="Thumbnail for Catching up with async Rust"
&gt;
&lt;/figure&gt;
&lt;/div&gt;

&lt;h2 id="testing"&gt;Testing&lt;/h2&gt;
&lt;p&gt;Something possessed me to go full enterprise software sicko mode during the development of &lt;code&gt;systemctl-bot&lt;/code&gt; and unit test every module to as close to 100% coverage as possible. I’m glad I did because it taught me more about generics and about Box, Rc, and Arc as I tried to find ways to mock dependencies, but it also taught me that this style of testing in Rust produces a huge glob of code that is painful to wrangle.&lt;/p&gt;
&lt;p&gt;I decided to take a different approach while developing &lt;code&gt;pipe-bot&lt;/code&gt;: I just mocked the outer edges of my program and let every test be an integration test. Any unit-level errors that mattered seem to come up in these tests, and since my program was small it wasn’t difficult to identify the specific function where the error originated. I got 99% of the benefit of unit testing with 20% of the effort.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I enjoy Rust, but I respect Go. Rust is more fun to write, and the compiler’s strict checking is a superpower that ensures you don’t screw yourself up too badly. However, async Rust is a huge pain for me, and while Go is boring, sometimes it’s the ticket to complete a project.&lt;/p&gt;</description></item><item><title>Keeping NixOS systems up to date with GitHub Actions</title><pubDate>Tue, 03 Sep 2024 12:00:00 -0400</pubDate><link>https://forrestjacobs.com/keeping-nixos-systems-up-to-date-with-github-actions/</link><guid>https://forrestjacobs.com/keeping-nixos-systems-up-to-date-with-github-actions/</guid><description>&lt;p&gt;Keeping my NixOS servers up to date was dead simple before I switched to flakes &amp;ndash; I enabled
&lt;a href="https://search.nixos.org/options?show=system.autoUpgrade.enable"&gt;system.autoUpgrade&lt;/a&gt;, and I was good to go. Trying the same with a shared flakes-based config introduced a
few problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I configured &lt;code&gt;autoUpgrade&lt;/code&gt; to commit flake lock changes, but it ran as &lt;em&gt;root&lt;/em&gt;. This created file permission issues
since my user owned my NixOS config.&lt;/li&gt;
&lt;li&gt;Even when committing worked, each machine piled up slightly different commits waiting for me to upstream.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I could have fixed issue #1 by changing the owner, but fixing #2 required me to rethink the process. Instead of having
each individual machine update their lock file, I realized it would be cleaner to update the lock file upstream &lt;em&gt;first&lt;/em&gt;,
and then rebuild each server from upstream. Updating the lock file first ensures there&amp;rsquo;s only one version of history,
and that makes it easier to reason about what is installed on each server.&lt;/p&gt;
&lt;p&gt;Below is one method of updating the shared lock file before updating each server:&lt;/p&gt;
&lt;h2 id="updating-flakelock-with-github-actions"&gt;Updating flake.lock with GitHub Actions&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://github.com/DeterminateSystems/update-flake-lock"&gt;&lt;em&gt;update-flake-lock&lt;/em&gt; GitHub Action&lt;/a&gt; updates your project&amp;rsquo;s flake lock file on a schedule. It essentially runs
&lt;code&gt;nix flake update --commit-lock-file&lt;/code&gt; and then opens a pull request. Add it to your NixOS config repository like this:&lt;/p&gt;
&lt;figure class="codeblock wide"&gt;
&lt;figcaption class="name"&gt;/.github/workflows/main.yml&lt;/figcaption&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 23.5625rem;"
&gt;&lt;code class="language-yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;update-dependencies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# allows manual triggering&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;0 6 * * *&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# daily at 1 am EST/2 am EDT&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;update-dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DeterminateSystems/nix-installer-action@v12&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;update&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DeterminateSystems/update-flake-lock@v23&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Add this step if you want to automatically merge the pull request:&lt;/p&gt;
&lt;figure class="codeblock wide"&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 10.5625rem;"
&gt;&lt;code class="language-yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Merge&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;gh pr merge --auto &amp;#34;${{ steps.update.outputs.pull-request-number }}&amp;#34; --rebase&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{secrets.GITHUB_TOKEN}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ steps.update.outputs.pull-request-number != &amp;#39;&amp;#39; }}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;h2 id="pulling-changes--rebuilding"&gt;Pulling changes &amp;amp; rebuilding&lt;/h2&gt;
&lt;p&gt;Next, it&amp;rsquo;s time to configure NixOS to pull changes and rebuild. The configuration below adds two &lt;em&gt;systemd&lt;/em&gt; services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pull-updates&lt;/code&gt; pulls config changes from upstream daily at 4:40. It has a few guardrails: it ensures the local
repository is on the main branch, and it only permits fast-forward merges. You&amp;rsquo;ll want to set &lt;code&gt;serviceConfig.User&lt;/code&gt; to
the user owning the repository. If it succeeds, it kicks off &lt;code&gt;rebuild&lt;/code&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rebuild&lt;/code&gt; rebuilds and switches to the new configuration, and reboots if required. It&amp;rsquo;s
&lt;a href="https://github.com/NixOS/nixpkgs/blob/6e99f2a27d600612004fbd2c3282d614bfee6421/nixos/modules/tasks/auto-upgrade.nix#L209-L256"&gt;a simplified version of &lt;code&gt;autoUpgrade&lt;/code&gt;&amp;rsquo;s script&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class="codeblock wide"&gt;
&lt;figcaption&gt;NixOS Config&lt;/figcaption&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 56.0625rem;"
&gt;&lt;code class="language-nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pull-updates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Pulls changes to system config&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;restartIfChanged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;onSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;rebuild.service&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;startAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;04:40&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openssh&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; test &amp;#34;$(git branch --show-current)&amp;#34; = &amp;#34;main&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; git pull --ff-only
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;serviceConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;WorkingDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/etc/nixos&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user-that-owns-the-repo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;oneshot&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rebuild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Rebuilds and activates system config&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;restartIfChanged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixos-rebuild&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; nixos-rebuild boot
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; booted=&amp;#34;$(readlink /run/booted-system/{initrd,kernel,kernel-modules})&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; built=&amp;#34;$(readlink /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; if [ &amp;#34;&lt;/span&gt;&lt;span class="se"&gt;&amp;#39;&amp;#39;$&lt;/span&gt;&lt;span class="s1"&gt;{booted}&amp;#34; = &amp;#34;&lt;/span&gt;&lt;span class="se"&gt;&amp;#39;&amp;#39;$&lt;/span&gt;&lt;span class="s1"&gt;{built}&amp;#34; ]; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; nixos-rebuild switch
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; reboot now
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;serviceConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;oneshot&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;There are many possible variations. For example, in my real config I split the pull service into separate fetch and
merge services so I can fetch more frequently. You could also replace the GitHub action with a different scheduled
script, or change the rebuild service to never (or always!) reboot.&lt;/p&gt;</description></item><item><title>Waiting on Tailscale</title><pubDate>Tue, 27 Aug 2024 12:00:00 -0400</pubDate><link>https://forrestjacobs.com/waiting-on-tailscale/</link><guid>https://forrestjacobs.com/waiting-on-tailscale/</guid><description>&lt;p&gt;I restarted my server the other day, and I realized one of my systemd services failed to start on boot because the
&lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; IP address was not assignable:&lt;/p&gt;
&lt;pre&gt;&lt;samp&gt;# journalctl -u bad-bad-not-good.service
...
listen tcp 100.11.22.33:8080: bind: cannot assign requested address&lt;/samp&gt;&lt;/pre&gt;
&lt;p&gt;This is easy enough to fix. The service should wait to start until after Tailscale is online, so let&amp;rsquo;s just add
&lt;code&gt;tailscaled.service&lt;/code&gt; to the the service&amp;rsquo;s &lt;code&gt;wants&lt;/code&gt; and &lt;code&gt;after&lt;/code&gt; properties, reboot, and&amp;hellip;&lt;/p&gt;
&lt;pre&gt;&lt;samp&gt;# journalctl -u bad-bad-not-good.service
...
listen tcp 100.11.22.33:8080: bind: cannot assign requested address&lt;/samp&gt;&lt;/pre&gt;
&lt;p&gt;Huh. It turns out Tailscale comes up a bit before its IP address is available. I was tempted to add an &lt;code&gt;ExecStartPre&lt;/code&gt;
to my service to sleep for 1 second &amp;ndash; gross! &amp;ndash; but eventually I found systemd&amp;rsquo;s fabulous
&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-networkd-wait-online.service.html"&gt;&lt;code&gt;systemd-networkd-wait-online&lt;/code&gt;&lt;/a&gt; command, which exits when a given interface has an IP
address. Call it with &lt;code&gt;-i [interface name]&lt;/code&gt; and either &lt;code&gt;-4&lt;/code&gt; or &lt;code&gt;-6&lt;/code&gt; to wait for an IPv4 or IPv6 address.&lt;/p&gt;
&lt;p&gt;Wrapping it up into a service gives you something like this:&lt;/p&gt;
&lt;figure class="codeblock"&gt;
&lt;figcaption class="name"&gt;tailscale-online.service&lt;/figcaption&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 17.0625rem;"
&gt;&lt;code class="language-ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Wait for Tailscale to have an IPv4 address&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Requisite&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;systemd-networkd.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;systemd-networkd.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Conflicts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;shutdown.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/lib/systemd/systemd-networkd-wait-online -i tailscale0 -4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Services using your Tailscale IP address can now depend on &lt;code&gt;tailscale-online&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Using Syncthing to sync coding projects</title><pubDate>Sat, 06 May 2023 12:00:00 -0400</pubDate><link>https://forrestjacobs.com/using-syncthing-to-sync-coding-projects/</link><guid>https://forrestjacobs.com/using-syncthing-to-sync-coding-projects/</guid><description>&lt;p&gt;I code on a MacBook and a Windows PC, and I want to keep my coding projects in sync between them. Here are my wishes in decreasing priority:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Code changes on my MacBook should magically update my PC, and vice versa. (Think Dropbox.)&lt;/li&gt;
&lt;li&gt;Some files should not sync, like host-specific dependencies and targets. I want to ignore these files via patterns, a la &lt;a href="https://git-scm.com/docs/gitignore"&gt;gitignore&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Ideally, this sync extends to both a headless Linux server I use for remote development, &lt;em&gt;and&lt;/em&gt; to &lt;a href="https://forrestjacobs.com/nixos-on-wsl/"&gt;WSL&lt;/a&gt; on my Windows PC.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After experimenting with other solutions (outlined below) I discovered that Syncthing meets every requirement.&lt;/p&gt;
&lt;h2 id="what-i-tried-first"&gt;What I tried first&lt;/h2&gt;
&lt;h3 id="1-onedrive"&gt;1. OneDrive&lt;/h3&gt;
&lt;p&gt;I use OneDrive to sync most of my files. It&amp;rsquo;d be nice to just add my coding projects to OneDrive, but it doesn&amp;rsquo;t work in practice: &lt;a href="https://superuser.com/a/1662761"&gt;ignoring files is awkward&lt;/a&gt;, and seemingly only works on Windows. Additionally, OneDrive doesn&amp;rsquo;t run on Linux without &lt;a href="https://rclone.org/onedrive/"&gt;some&lt;/a&gt; &lt;a href="https://github.com/abraunegg/onedrive"&gt;help&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Dropbox looks like a better fit on paper: &lt;a href="https://help.dropbox.com/sync/ignored-files"&gt;it can ignore files on any platform&lt;/a&gt; (in a different, awkward way) and &lt;a href="https://www.dropbox.com/install-linux"&gt;it has a first party Linux client&lt;/a&gt;. But switching to Dropbox would be painful &amp;ndash; my partner and I switched &lt;em&gt;away&lt;/em&gt; from Dropbox about a year ago because we were getting more storage for less money from Microsoft, and &lt;a href="https://daringfireball.net/linked/2019/06/13/dropbox-sucks"&gt;the modern Dropbox app sucks&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="2-remote-development"&gt;2. Remote development&lt;/h3&gt;
&lt;p&gt;If the issue is syncing files across computers, &lt;a href="https://justsimply.dev/"&gt;why don&amp;rsquo;t I just&lt;/a&gt; work on one computer? Well, developing on a remote machine has its own issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Blips in internet connectivity become big problems. At best, you wait for keystrokes to appear over SSH. At worst, you can&amp;rsquo;t code at all. (And while file sync also requires connectivity, a few seconds is enough to sync changes.)&lt;/li&gt;
&lt;li&gt;Waiting for my dinky &lt;a href="https://www.oracle.com/cloud/free/"&gt;free-tier Oracle Cloud VM&lt;/a&gt; to compile a complex Rust project is frustrating. Sure, I could rent a better VM, but it&amp;rsquo;s silly to pay for that additional power when I have a more than capable computer in front of me.&lt;/li&gt;
&lt;li&gt;Some development doesn&amp;rsquo;t work well in a remote environment. Web dev is fine, but what if I want to play around with &lt;a href="https://bevyengine.org/"&gt;game&lt;/a&gt; or mobile dev?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-git"&gt;3. Git&lt;/h3&gt;
&lt;p&gt;Can&amp;rsquo;t I just use Git to stay up to date?&lt;/p&gt;
&lt;p&gt;No &amp;ndash; version control is different than file sync. I don&amp;rsquo;t want to track personal config files in version control, but I &lt;em&gt;do&lt;/em&gt; want to sync them. And I don&amp;rsquo;t always want to check in work in progress &amp;ndash; for example, I don&amp;rsquo;t want to check in changes that cause builds or tests to fail.&lt;/p&gt;
&lt;h2 id="using-syncthing"&gt;Using Syncthing&lt;/h2&gt;
&lt;p&gt;Syncthing is amazing. It does everything I outlined at the top &amp;ndash; it syncs my projects, it ignores files based on patterns, and it runs everywhere I code (Windows, MacOS, and Linux.)&lt;/p&gt;
&lt;p&gt;I resisted using it because of its high barrier to entry. It uses peer-to-peer file syncing, so you need to set it up on a server to ensure each computer sees the latest changes. And its configuration is more involved than something like Dropbox.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s still worth it for me because it solves all my original problems. (And I want to sync these files to a server anyway.) If you&amp;rsquo;re struggling with the same issues I ran into, and you&amp;rsquo;re willing to set up a server, give Syncthing a shot.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="addendum-syncing-your-ignore-patterns"&gt;Addendum: Syncing your ignore patterns&lt;/h2&gt;
&lt;p&gt;Syncthing does not keep your ignore patterns in sync across hosts, but there&amp;rsquo;s a way around it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a text file with the patterns you want to ignore.&lt;/li&gt;
&lt;li&gt;Save it to your Syncthing folder.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;#include &lt;em&gt;name-of-that-file&lt;/em&gt;&lt;/code&gt; to each host&amp;rsquo;s ignore patterns.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Voilà!&lt;/p&gt;</description></item><item><title>NixOS on WSL</title><pubDate>Sun, 22 Jan 2023 12:00:00 -0500</pubDate><link>https://forrestjacobs.com/nixos-on-wsl/</link><guid>https://forrestjacobs.com/nixos-on-wsl/</guid><description>&lt;p&gt;I recently set up a new Windows machine for gaming, but I&amp;rsquo;m actually using it more to play around with Linux via &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/about"&gt;WSL&lt;/a&gt;.
I set up &lt;a href="https://nixos.org/"&gt;NixOS&lt;/a&gt; on WSL using the aptly named &lt;a href="https://github.com/nix-community/NixOS-WSL"&gt;NixOS on WSL&lt;/a&gt; project, but ran into a few issues during setup that
bricked my environment&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. Below are the steps that ended up working for me in the end:&lt;/p&gt;
&lt;h2 id="goals"&gt;Goals&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Set up NixOS in WSL 2 on a new Windows 11 install (version 22H2.)&lt;/li&gt;
&lt;li&gt;Reuse my flake-based configuration from my other NixOS 22.11 installations.&lt;/li&gt;
&lt;li&gt;Take advantage of &lt;a href="https://devblogs.microsoft.com/commandline/systemd-support-is-now-available-in-wsl/"&gt;native &lt;em&gt;systemd&lt;/em&gt; support in WSL&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-1-install-wsl-2"&gt;Step 1: Install WSL 2&lt;/h2&gt;
&lt;p&gt;Per &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/install#install-wsl-command"&gt;Microsoft&amp;rsquo;s documentation&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open PowerShell as an admin by right-clicking on it and selecting &amp;ldquo;Run as administrator.&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;wsl --install&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="step-2-install-nixos"&gt;Step 2: Install NixOS&lt;/h2&gt;
&lt;p&gt;Per &lt;a href="https://github.com/nix-community/NixOS-WSL#quick-start"&gt;NixOS-WSL&amp;rsquo;s documentation&lt;/a&gt;:&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;
&lt;p&gt;Download the installer listed in &lt;a href="https://github.com/nix-community/NixOS-WSL/releases/tag/22.05-5c211b47"&gt;NixOS-WSL&amp;rsquo;s 22.05 release&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Move the file where you want your NixOS installation to live. I put it in
&lt;code&gt;C:\Users\&amp;lt;my username&amp;gt;\AppData\Local\NixOS&lt;/code&gt;, which I &lt;em&gt;think&lt;/em&gt; follows Windows&amp;rsquo;s conventions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open PowerShell as a normal user, change to the directory you just made, and run
&lt;code&gt;wsl --import NixOS . nixos-wsl-installer.tar.gz --version 2&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;wsl -d NixOS&lt;/code&gt; to start NixOS. It&amp;rsquo;ll run through initial setup, then hang on &amp;ldquo;Starting systemd&amp;hellip;&amp;rdquo; Press
&lt;kbd&gt;Ctrl&lt;/kbd&gt; + &lt;kbd&gt;C&lt;/kbd&gt; to get back to PowerShell, and then run &lt;code&gt;wsl --shutdown&lt;/code&gt; followed by &lt;code&gt;wsl -d NixOS&lt;/code&gt;
to get back into NixOS.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="step-3-configure-nixos"&gt;Step 3: Configure NixOS&lt;/h2&gt;
&lt;p&gt;At this point you should have a basic NixOS 22.05 installation! Let&amp;rsquo;s finish up by setting a username, switching over to
flakes, updating to NixOS 22.11, and enabling native &lt;em&gt;systemd&lt;/em&gt;:&lt;/p&gt;
&lt;ol start="7"&gt;
&lt;li&gt;
&lt;p&gt;Edit &lt;code&gt;/etc/nixos/configuration.nix&lt;/code&gt; and make the following changes. Use &lt;code&gt;sudo&lt;/code&gt; or some such to edit the file as
&lt;em&gt;root&lt;/em&gt;. You may want to &lt;code&gt;nix-shell -p&lt;/code&gt; your favorite editor at this point &amp;ndash; maybe &lt;a href="https://helix-editor.com/"&gt;Helix&lt;/a&gt;?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Change &lt;code&gt;wsl.defaultUser&lt;/code&gt; to your desired username, and add something like
&lt;code&gt;users.users.&amp;lt;your username&amp;gt;.isNormalUser = true;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Remove references to &lt;code&gt;./nixos-wsl&lt;/code&gt; since we&amp;rsquo;ll set NixOS-WSL as a flake input:
&lt;figure class="codeblock"&gt;
&lt;figcaption class="name"&gt;/etc/nixos/configuration.nix&lt;/figcaption&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 20.3125rem;"
&gt;&lt;code class="language-diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { lib, pkgs, config, modulesPath, ... }:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;-
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- with lib;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- let
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- nixos-wsl = import ./nixos-wsl;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; imports = [
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;${modulesPath}/profiles/minimal.nix&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;-
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- nixos-wsl.nixosModules.wsl
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt; ];
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;wsl.nativeSystemd = true;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;networking.hostName&lt;/code&gt;, and set it to your Windows 11 &amp;ldquo;Device name.&amp;rdquo; You can find this in the Windows Settings
app under &lt;em&gt;System&lt;/em&gt; &amp;gt; &lt;em&gt;About&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add &lt;code&gt;/etc/nixos/flake.nix&lt;/code&gt; as &lt;em&gt;root&lt;/em&gt; with contents like the following. Replace &lt;code&gt;&amp;lt;host-name&amp;gt;&lt;/code&gt; with your actual host
name from &lt;code&gt;configuration.nix&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="codeblock"&gt;
&lt;figcaption class="name"&gt;/etc/nixos/flake.nix&lt;/figcaption&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 36.5625rem;"
&gt;&lt;code class="language-nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:NixOS/nixpkgs/nixos-22.11&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;NixOS-WSL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:nix-community/NixOS-WSL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;follows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;nixpkgs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NixOS-WSL&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixosConfigurations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;lt;host-name&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixosSystem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x86_64-linux&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;nix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sr"&gt;./configuration.nix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;NixOS-WSL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixosModules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;sudo nixos-rebuild switch&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;exit&lt;/code&gt; out of NixOS, then run &lt;code&gt;wsl --shutdown&lt;/code&gt; followed by &lt;code&gt;wsl -d NixOS&lt;/code&gt;. You should be logged in as the new
default user.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At this point you should have a stable base to make any configuration changes you&amp;rsquo;d like.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="step-4-cleaning-up"&gt;Step 4: Cleaning up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Rename &lt;code&gt;wsl.automountPath&lt;/code&gt; to &lt;code&gt;wsl.wslConf.automount.root&lt;/code&gt; in &lt;code&gt;configuration.nix&lt;/code&gt; to match NixOS-WSL&amp;rsquo;s latest
configuration.&lt;/li&gt;
&lt;li&gt;Delete the now unused &lt;code&gt;/etc/nixos/nixos-wsl&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Delete &lt;code&gt;/home/nixos&lt;/code&gt; since you&amp;rsquo;ve changed your username.&lt;/li&gt;
&lt;li&gt;You can run &lt;code&gt;sudo nix-channel --remove nixos&lt;/code&gt; if you don&amp;rsquo;t want the channel hanging around now that we&amp;rsquo;re using
flakes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="addendum-share-ssh-directory-with-windows"&gt;Addendum: Share &lt;code&gt;.ssh&lt;/code&gt; directory with Windows&lt;/h2&gt;
&lt;p&gt;It turns out that &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/file-permissions"&gt;file permissions set in WSL are preserved in NTFS&lt;/a&gt;&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. You can store your SSH
configuration files in Windows, and still use them from WSL with no permissions issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up your &lt;code&gt;~/.ssh&lt;/code&gt; in NixOS like normal.&lt;/li&gt;
&lt;li&gt;Move your &lt;code&gt;~/.ssh&lt;/code&gt; directory to Windows with &lt;code&gt;mv ~/.ssh /mnt/c/Users/&amp;lt;your Windows username&amp;gt;/.ssh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Soft link it back to your WSL VM with &lt;code&gt;ln -s /mnt/c/Users/&amp;lt;your Windows username&amp;gt;/.ssh/ ~/.ssh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s it! You can verify that permissions carried over with &lt;code&gt;ls -al ~/.ssh/&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="addendum-set-up-fish-in-nixos-the-right-way"&gt;Addendum: Set up &lt;em&gt;fish&lt;/em&gt; in NixOS the right way&lt;/h2&gt;
&lt;p&gt;I use &lt;a href="https://fishshell.com/"&gt;&lt;em&gt;fish&lt;/em&gt;&lt;/a&gt; as my shell, but when I set it up in WSL I started getting errors like this:&lt;/p&gt;
&lt;pre&gt;&lt;samp&gt;fish: Unknown command: ls
/nix/store/lkf5vmavnxa0s37imb03gv7hs6dh5pll-fish-3.5.1/share/fish/functions/ls.fish (line 64):
 command $__fish_ls_command $__fish_ls_color_opt $opt $argv
 ^
in function &amp;#39;ls&amp;#39;&lt;/samp&gt;&lt;/pre&gt;
&lt;p&gt;Uh oh. Obviously, there are important directories missing from my path. It turns out that I was missing some
configuration&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;figure class="codeblock"&gt;
&lt;figcaption&gt;NixOS Config&lt;/figcaption&gt;
&lt;pre
 role="document" tabindex="0" class="chroma"
 style="line-height: 1.625rem;height: 5.6875rem;"
&gt;&lt;code class="language-nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;programs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fish&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I don&amp;rsquo;t know why this issue only appeared in WSL, but &lt;em&gt;fish&lt;/em&gt; users should probably set &lt;code&gt;programs.fish.enable&lt;/code&gt; in NixOS
whether the issue appears or not.&lt;/p&gt;
&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://xeiaso.net/blog/nix-flakes-4-wsl-2022-05-01"&gt;Nix Flakes on WSL&lt;/a&gt; by &lt;a href="https://xeiaso.net/"&gt;Xe Iaso&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;I &lt;em&gt;think&lt;/em&gt; I changed the default user without actually setting up that user? Or I switched to native
&lt;em&gt;systemd&lt;/em&gt; before upgrading to a version of NixOS on WSL that supports it?&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Thanks to &lt;a href="https://superuser.com/a/1334839"&gt;this Stack Exchange answer&lt;/a&gt; for pointing me in the
right direction. You don&amp;rsquo;t have to follow the instructions in this post &amp;ndash; NixOS on WSL already mounts Windows&amp;rsquo;s drives
with the necessary options.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Thanks to &lt;a href="https://github.com/nix-community/NixOS-WSL/issues/192"&gt;this GitHub issue&lt;/a&gt; for alerting
me to the problem.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item></channel></rss>