<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>chris' random ramblings</title><link>https://atlee.ca/</link><description>programming, photography, media, and anything else that strikes my fancy</description><atom:link href="https://atlee.ca/rss.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><lastBuildDate>Sun, 04 Oct 2026 16:31:35 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>How to fix Let's Encrypt / acme.sh renewal on Synology</title><link>https://atlee.ca/posts/synology-acme-renewal/</link><dc:creator>chris</dc:creator><description>&lt;p&gt;I use a &lt;a href="https://letsencrypt.org/"&gt;Let's Encrypt&lt;/a&gt; certificate so &lt;a href="https://www.synology.com/en-us/dsm/feature/photos"&gt;Synology Photos&lt;/a&gt; can connect to my NAS over HTTPS without a certificate warning. I renew it with &lt;a href="https://github.com/acmesh-official/acme.sh"&gt;&lt;code&gt;acme.sh&lt;/code&gt;&lt;/a&gt; using a DNS challenge.&lt;/p&gt;
&lt;p&gt;When the certificate expired, Photos stopped syncing and renewal failed with:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;curl: (60) SSL certificate problem: certificate has expired
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The error made it look like &lt;code&gt;acme.sh&lt;/code&gt; didn't trust a certificate authority. The actual problem was that the &lt;a href="https://www.rfc-editor.org/rfc/rfc8484"&gt;DNS-over-HTTPS (DoH)&lt;/a&gt; request was reaching my NAS instead of Cloudflare.&lt;/p&gt;
&lt;h3 id="finding-the-wrong-certificate"&gt;Finding the wrong certificate&lt;/h3&gt;
&lt;p&gt;During DNS validation, the renewal script queried &lt;code&gt;cloudflare-dns.com&lt;/code&gt; and &lt;code&gt;dns.google&lt;/code&gt; over DNS-over-HTTPS. I checked the certificate returned for Cloudflare:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;s_client&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-connect&lt;span class="w"&gt; &lt;/span&gt;cloudflare-dns.com:443&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-servername&lt;span class="w"&gt; &lt;/span&gt;cloudflare-dns.com&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-showcerts&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;
openssl&lt;span class="w"&gt; &lt;/span&gt;x509&lt;span class="w"&gt; &lt;/span&gt;-noout&lt;span class="w"&gt; &lt;/span&gt;-subject&lt;span class="w"&gt; &lt;/span&gt;-issuer&lt;span class="w"&gt; &lt;/span&gt;-dates
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The subject was my NAS's hostname. A local lookup showed that &lt;code&gt;cloudflare-dns.com&lt;/code&gt; resolved to &lt;code&gt;0.0.0.0&lt;/code&gt; and &lt;code&gt;::&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;nslookup&lt;span class="w"&gt; &lt;/span&gt;cloudflare-dns.com
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;My router uses a NextDNS profile with &lt;strong&gt;Block Bypass Methods&lt;/strong&gt; enabled. It was blocking the DNS-over-HTTPS providers and returning those addresses. The connection went back to the NAS, which presented its expired certificate. That's why &lt;code&gt;curl&lt;/code&gt; complained about an expired certificate while &lt;code&gt;acme.sh&lt;/code&gt; was trying to contact Cloudflare.&lt;/p&gt;
&lt;p&gt;I checked the same name using Cloudflare's resolver directly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;nslookup&lt;span class="w"&gt; &lt;/span&gt;cloudflare-dns.com&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.1.1.1
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That returned the expected address. I added &lt;code&gt;cloudflare-dns.com&lt;/code&gt; and &lt;code&gt;dns.google&lt;/code&gt; to the NextDNS allowlist.&lt;/p&gt;
&lt;h3 id="clearing-the-failed-challenge-and-renewing"&gt;Clearing the failed challenge and renewing&lt;/h3&gt;
&lt;p&gt;The first attempt had created the ACME DNS TXT record before it failed. After allowing the DNS-over-HTTPS providers, the next attempt stopped with Cloudflare error 81058 because that TXT record already existed. I deleted the stale &lt;code&gt;_acme-challenge&lt;/code&gt; record from Cloudflare, checked that it was gone, and ran the renewal again. I also upgraded &lt;code&gt;acme.sh&lt;/code&gt; from 3.0.9 to 3.1.5. The script deployed the new certificate into DSM with &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix for this error was the NextDNS allowlist. The certificate subject gave away the real problem: I was seeing my NAS's certificate in a connection that should have gone to Cloudflare.&lt;/p&gt;
&lt;h3 id="separately-adding-ca-roots-to-dsm"&gt;Separately: adding CA roots to DSM&lt;/h3&gt;
&lt;p&gt;I also updated DSM's CA roots, but that was separate maintenance, not the fix above. On DSM 7, custom root certificates go in this directory as &lt;code&gt;.crt&lt;/code&gt; files:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;/usr/syno/etc/security-profile/ca-bundle-profile/ca-certificates/
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With SSH enabled, I copied the current ISRG Root X1 and X2 certificates from &lt;a href="https://letsencrypt.org/certificates/"&gt;Let's Encrypt&lt;/a&gt; into that directory and rebuilt DSM's trust bundle:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/usr/syno/etc/security-profile/ca-bundle-profile/ca-certificates
sudo&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;isrgrootx1.pem&lt;span class="w"&gt; &lt;/span&gt;/usr/syno/etc/security-profile/ca-bundle-profile/ca-certificates/isrg-root-x1.crt
sudo&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;isrg-root-x2.pem&lt;span class="w"&gt; &lt;/span&gt;/usr/syno/etc/security-profile/ca-bundle-profile/ca-certificates/isrg-root-x2.crt
sudo&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;644&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/usr/syno/etc/security-profile/ca-bundle-profile/ca-certificates/*.crt
sudo&lt;span class="w"&gt; &lt;/span&gt;/usr/syno/bin/update-ca-certificates.sh
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This updates roots DSM uses to verify certificates from other services. It doesn't change the certificate DSM presents to Photos clients; that is managed under &lt;strong&gt;Control Panel → Security → Certificate&lt;/strong&gt;. Synology's &lt;a href="https://kb.synology.com/en-us/DSM/help/DSM/AdminCenter/connection_certificate"&gt;certificate guide&lt;/a&gt; covers that separate step.&lt;/p&gt;</description><category>linux</category><category>networking</category><category>synology</category><guid>https://atlee.ca/posts/synology-acme-renewal/</guid><pubDate>Sun, 27 Sep 2026 04:00:00 GMT</pubDate></item><item><title>Making a Vim watch face glow</title><link>https://atlee.ca/posts/vim-glow-watch-face/</link><dc:creator>chris</dc:creator><description>&lt;p&gt;I wanted a Vim styled watch face for my Garmin Fenix 8. Line numbers, a filename at the top, and status information along the bottom. The time and other watch data could be the contents of the file.&lt;/p&gt;
&lt;p&gt;I also wanted it to look like an old green CRT, with glowing letters and scanlines across the display. Getting the text onto the screen was straightforward. Getting it to glow took quite a few attempts. Here's where I ended up with &lt;strong&gt;Vim Glow&lt;/strong&gt;:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src="https://atlee.ca/posts/vim-glow-watchface-1.png" alt="Vim Glow running in the Garmin Fenix 8 simulator, showing green terminal text, scanlines, and a large glowing clock"&gt;
&lt;/figure&gt;

&lt;h3 id="getting-the-clock-to-glow"&gt;Getting the clock to glow&lt;/h3&gt;
&lt;p&gt;My first approach was to build bitmap font atlases from Berkeley Mono using &lt;a href="https://www.angelcode.com/products/bmfont/"&gt;BMFont&lt;/a&gt;. An atlas puts the characters into one image; the watch draws the part of the image containing the character it needs.&lt;/p&gt;
&lt;p&gt;The plan was to layer blurred copies behind the sharp green letters. I tried fonts at different sizes and overlapping passes to get a soft halo around the text. That meant loading several large atlases. The first version loaded nine and crashed when the simulator ran out of graphics memory.&lt;/p&gt;
&lt;p&gt;Smaller atlases helped with memory, but I still couldn't get the glow to blend correctly. The blur was visible in the generated PNGs, yet the clock looked sharp in the simulator. Drawing the blurred atlas on its own produced a black rectangle around it. Other transparent glyph images disappeared entirely. I tried blend modes and a bitmap buffer with alpha blending enabled, but couldn't get transparency working reliably.&lt;/p&gt;
&lt;p&gt;Eventually I moved the clock's glow into the images themselves. I generated a separate image for each digit and the colon, with the blur and sharp letter already combined against a black background. The watch only has to draw the finished image.&lt;/p&gt;
&lt;p&gt;The build script crops each character out of the atlas, then uses ImageMagick to blur its mask:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nx"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"magick"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;sharpPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"-alpha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"extract"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"-blur"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`0x&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blurRadius&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"-evaluate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multiply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;glowAlpha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;maskPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It colours that blurred mask green and combines it with the sharp character on black. All the images use the same cell width, so the clock keeps its monospace spacing. Finally, the digits had the glow I'd been trying to get.&lt;/p&gt;
&lt;h3 id="putting-the-scanlines-back"&gt;Putting the scanlines back&lt;/h3&gt;
&lt;p&gt;The scanlines had been troublesome for the same reason. I'd been drawing them behind the text, expecting them to show through the transparent parts of the letters. The opaque clock images now painted black rectangles over them. I had a glowing clock, but the scanlines stopped wherever a character was drawn.&lt;/p&gt;
&lt;p&gt;The fix was to draw the scanlines last, over the letters and their glow. Multiply blending darkens the pixels already on screen:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drawScanlines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;awake&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;awake&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SCANLINE_WAKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SCANLINE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setBlendMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Graphics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLEND_MODE_MULTIPLY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BG&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HEIGHT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drawLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WIDTH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setBlendMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Graphics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLEND_MODE_DEFAULT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now the lines run across the whole face, including the clock. I'd originally expected to build the effect from transparent layers, but the opaque character images with scanlines drawn over them still give it the look I wanted.&lt;/p&gt;
&lt;h3 id="running-the-simulator-locally"&gt;Running the simulator locally&lt;/h3&gt;
&lt;p&gt;I ran Garmin's Connect IQ simulator in Docker. It needs older libraries, including GTK and WebKitGTK, and I wanted to keep those dependencies off my main system. The container runs the simulator window on my desktop, so I can load the watch face and see how the glow and scanlines look as I change them.&lt;/p&gt;
&lt;h3 id="try-vim-glow"&gt;Try Vim Glow&lt;/h3&gt;
&lt;p&gt;If you want to try it on your Fenix 8, I made a &lt;a href="https://atlee.ca/vim-glow/"&gt;web installer&lt;/a&gt;. Plug in the watch and open the page in Chrome or Edge on a desktop computer.&lt;/p&gt;</description><category>garmin</category><category>vim</category><category>watchface</category><guid>https://atlee.ca/posts/vim-glow-watch-face/</guid><pubDate>Sun, 27 Sep 2026 04:00:00 GMT</pubDate></item><item><title>need: for when you just need simple build dependencies</title><link>https://atlee.ca/posts/need-build-dependencies/</link><dc:creator>chris</dc:creator><description>&lt;p&gt;Sometimes I need to express a simple dependency between files. For example, if I have a directory full of images, I want to generate a thumbnail for each image, and only regenerate the thumbnails that are out of date.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/casey/just"&gt;&lt;code&gt;just&lt;/code&gt;&lt;/a&gt; is great as a generic command runner. I frequently set up a &lt;code&gt;justfile&lt;/code&gt; with common targets like &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt;, and &lt;code&gt;clean&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;just&lt;/code&gt; doesn't track file dependencies. I could write a recipe that loops over all the images:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nf"&gt;thumbs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;images/*.jpg&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;magick&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-thumbnail&lt;span class="w"&gt; &lt;/span&gt;200x200&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thumbs/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="p"&gt;#images/&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;thumbs
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This regenerates every thumbnail each time I run &lt;code&gt;just thumbs&lt;/code&gt;, even if most of them haven't changed.&lt;/p&gt;
&lt;h3 id="why-not-make"&gt;Why not Make?&lt;/h3&gt;
&lt;p&gt;Make can track those file dependencies:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;-&lt;span class="n"&gt;thumbs&lt;/span&gt; &lt;span class="n"&gt;clean&lt;/span&gt;
&lt;span class="nf"&gt;all-thumbs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;patsubst&lt;/span&gt; &lt;span class="nv"&gt;images&lt;/span&gt;/%&lt;span class="nv"&gt;.jpg&lt;/span&gt;,&lt;span class="nv"&gt;thumbs&lt;/span&gt;/%&lt;span class="nv"&gt;.jpg&lt;/span&gt;,&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;wildcard&lt;/span&gt; &lt;span class="nv"&gt;images&lt;/span&gt;/*&lt;span class="nv"&gt;.jpg&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;thumbs/%.jpg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;/%.&lt;span class="n"&gt;jpg&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;thumbs&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;magick&lt;span class="w"&gt; &lt;/span&gt;$&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;-thumbnail&lt;span class="w"&gt; &lt;/span&gt;200x200&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;

&lt;span class="nf"&gt;thumbs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;thumbs

&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;thumbs
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I've always found Make annoying for this kind of thing.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You have to mark command-like targets such as &lt;code&gt;clean&lt;/code&gt; and &lt;code&gt;all-thumbs&lt;/code&gt; as phony, or Make may mistake a file with that name for an up-to-date target.&lt;/li&gt;
&lt;li&gt;You have to create the output directory yourself.&lt;/li&gt;
&lt;li&gt;Recipes need literal tabs. I've lost far too much time because a Makefile was missing one.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="why-you-need-need"&gt;Why you need &lt;code&gt;need&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;I wrote &lt;a href="https://github.com/catlee/need"&gt;&lt;code&gt;need&lt;/code&gt;&lt;/a&gt; to address these annoyances. Need only handles file dependencies. It doesn't try to be a command runner.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;need&lt;/code&gt;, I express dependencies between files and give it a recipe for building the outputs from the inputs. &lt;code&gt;need&lt;/code&gt; compares file contents, so changing an input makes its outputs stale even if the timestamps are misleading. It also creates output directories automatically.&lt;/p&gt;
&lt;p&gt;So I keep my &lt;code&gt;justfile&lt;/code&gt; for running commands and add a &lt;code&gt;needfile&lt;/code&gt; to express file dependencies. The thumbnail example becomes:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;# justfile&lt;/span&gt;
&lt;span class="nf"&gt;thumbs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;need&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'thumbs/%.jpg: images/%.jpg'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;images/*.jpg

&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;need&lt;span class="w"&gt; &lt;/span&gt;clean
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;# needfile&lt;/span&gt;
&lt;span class="nf"&gt;thumbs/%.jpg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;/%.&lt;span class="n"&gt;jpg&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;magick&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{{&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="o"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-thumbnail&lt;span class="w"&gt; &lt;/span&gt;200x200&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{{&lt;/span&gt;out&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, the shell expands &lt;code&gt;images/*.jpg&lt;/code&gt; into the input filenames. &lt;code&gt;need get&lt;/code&gt; maps each &lt;code&gt;images/name.jpg&lt;/code&gt; to &lt;code&gt;thumbs/name.jpg&lt;/code&gt;, then builds those targets. The &lt;code&gt;-j&lt;/code&gt; option lets independent thumbnails build in parallel. If nothing changed, Need has nothing to do.&lt;/p&gt;
&lt;p&gt;Need can also publish outputs atomically, so a failed recipe doesn't leave a partially written thumbnail in place. Its dependency signatures use file contents, not timestamps alone. For a bigger example, it can use compiler depfiles to track headers discovered while compiling.&lt;/p&gt;
&lt;p&gt;I still use &lt;code&gt;just&lt;/code&gt; for commands like &lt;code&gt;test&lt;/code&gt; and &lt;code&gt;format&lt;/code&gt;. &lt;code&gt;need&lt;/code&gt; handles the file artifacts those commands depend on.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/catlee/need"&gt;need on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://crates.io/crates/need-tool"&gt;need-tool on crates.io&lt;/a&gt;&lt;/p&gt;</description><category>build tools</category><category>make</category><category>need</category><guid>https://atlee.ca/posts/need-build-dependencies/</guid><pubDate>Sat, 26 Sep 2026 21:57:14 GMT</pubDate></item><item><title>Snatching text from Herdr</title><link>https://atlee.ca/posts/herdr-snatch/</link><dc:creator>chris</dc:creator><description>&lt;p&gt;A couple of years ago I wrote about &lt;a href="https://atlee.ca/posts/extrakto/"&gt;Extrakto&lt;/a&gt;, a tmux plugin that lets me find text in my terminal and insert it into the command I'm typing. I've missed that little trick while working in &lt;a href="https://herdr.dev/"&gt;Herdr&lt;/a&gt;. A build prints the path I need, and there I am selecting it with the mouse or typing it out again.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href="https://github.com/catlee/herdr-snatch"&gt;herdr-snatch&lt;/a&gt;. It puts an &lt;code&gt;fzf&lt;/code&gt; picker over Herdr's output. Search for a path, URL, commit hash, word, or whole line, then press Enter to insert it into the pane you came from.&lt;/p&gt;
&lt;p&gt;For example, if a build prints &lt;code&gt;src/foo/parser.rs&lt;/code&gt;, I can type &lt;code&gt;vim&lt;/code&gt;, open Snatch with &lt;code&gt;prefix+Tab&lt;/code&gt;, search for &lt;code&gt;pars&lt;/code&gt;, and press Enter. The path appears after &lt;code&gt;vim&lt;/code&gt;, ready for me to run the command. Snatch doesn't press Enter for me, which is probably for the best.&lt;/p&gt;
&lt;video width="800" controls muted playsinline&gt;
    &lt;source src="https://atlee.ca/posts/herdr-snatch.mp4" type="video/mp4"&gt;
&lt;/source&gt;&lt;/video&gt;

&lt;p&gt;It searches the visible output in the current tab by default. If the text was in another pane, or has scrolled away, I can switch to the whole workspace or retained scrollback without losing my search. &lt;code&gt;Ctrl-Y&lt;/code&gt; copies the selection to the clipboard, and &lt;code&gt;Ctrl-O&lt;/code&gt; opens a URL or file instead of inserting it.&lt;/p&gt;
&lt;p&gt;To install it, you'll need Herdr 0.8.2 or newer, Python 3.9 or newer, and &lt;code&gt;fzf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;herdr&lt;span class="w"&gt; &lt;/span&gt;plugin&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;catlee/herdr-snatch
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://github.com/catlee/herdr-snatch#install"&gt;README&lt;/a&gt; has the key binding I use and the other picker shortcuts. Right now this is a small tool for the paths and bits of output I keep reusing. If it misses something useful in your terminal, let me know!&lt;/p&gt;</description><category>herdr</category><category>plugins</category><category>shell</category><guid>https://atlee.ca/posts/herdr-snatch/</guid><pubDate>Sat, 26 Sep 2026 18:21:53 GMT</pubDate></item><item><title>CO2 Monitoring with ESPHome</title><link>https://atlee.ca/posts/co2-esphome/</link><dc:creator>chris</dc:creator><description>&lt;h2 id="help-my-head-hurts-and-my-nose-is-burning-a-co2-wake-up-call"&gt;Help! My Head Hurts and My Nose is Burning: A CO2 Wake-Up Call 😵💨&lt;/h2&gt;
&lt;p&gt;For the past few years, I've been working from my small home office. This winter has been colder than usual, so I've been keeping the windows closed with little thought to the air around me. But then I started noticing a pattern — by the afternoon, headaches would creep in, and sometimes, upon reentering my office, I’d be hit with a strange, burning sensation in my nose. It smelled like iron, reminiscent of a bloody nose or the sharp tang of sticking my head into my fermentation chamber while brewing beer. Little did I know, these were all warning signs that something was off with my air quality.&lt;/p&gt;
&lt;p&gt;Monitoring indoor air quality has become increasingly important, especially with the growing awareness of CO2 levels and their impact on health and productivity.&lt;/p&gt;
&lt;p&gt;I'm a big fan of DIY solutions whenever possible, so I researched options available for hobbyist home electronics and found a great solution.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://atlee.ca/posts/co2_sensor.jpg"&gt;&lt;/p&gt;
&lt;h3 id="esphome-a-smart-diy-approach-to-air-quality-monitoring"&gt;ESPHome: A Smart DIY Approach to Air Quality Monitoring 🔧&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://esphome.io/"&gt;ESPHome&lt;/a&gt; is a powerful open-source framework that allows you to easily create custom firmware for ESP8266 and ESP32-based devices. It enables seamless integration with smart home platforms like Home Assistant and provides a user-friendly YAML-based configuration system. Whether you're a beginner or an experienced hobbyist, ESPHome simplifies the process of building and automating IoT devices.&lt;/p&gt;
&lt;p&gt;Using an &lt;a href="https://www.adafruit.com/product/4172"&gt;ESP8266 board&lt;/a&gt;, an &lt;a href="https://www.adafruit.com/product/5190"&gt;SCD41 CO2 sensor&lt;/a&gt;, and a &lt;a href="https://www.adafruit.com/product/1536"&gt;simple buzzer&lt;/a&gt;, I created a CO2 monitor for my office. This system integrates with &lt;a href="https://www.home-assistant.io/integrations/esphome"&gt;Home Assistant&lt;/a&gt; and generates an audible alarm when CO2 levels rise too high.&lt;/p&gt;
&lt;h3 id="setting-up-esphome"&gt;Setting up ESPHome&lt;/h3&gt;
&lt;h4 id="installation"&gt;Installation&lt;/h4&gt;
&lt;p&gt;Install ESPHome using the following command:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;esphome
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then create a configuration file for the sensor, e.g., &lt;code&gt;co2.yaml&lt;/code&gt;. My configuration looks something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;esphome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;co2&lt;/span&gt;

&lt;span class="nt"&gt;esp8266&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;board&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nodemcuv2&lt;/span&gt;

&lt;span class="nt"&gt;wifi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ssid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;wifi_ssid&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;wifi_password&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;fast_connect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="nt"&gt;web_server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;80&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;

&lt;span class="nt"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;hardware_uart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;uart1&lt;/span&gt;

&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;encryption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;api_key&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;rtttl_play&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;song_str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;string&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rtttl.play&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;rtttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!lambda&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;song_str;'&lt;/span&gt;

&lt;span class="nt"&gt;ota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;esphome&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ota_password&lt;/span&gt;

&lt;span class="nt"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;sda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;D1&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;scl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;D2&lt;/span&gt;

&lt;span class="nt"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;esp8266_pwm&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;D3&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;rtttl_out&lt;/span&gt;

&lt;span class="nt"&gt;rtttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;rtttl_out&lt;/span&gt;

&lt;span class="nt"&gt;sensor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;scd4x&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;co2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&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="s"&gt;"CO2"&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;co2&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&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="s"&gt;"Temperature"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;humidity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&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="s"&gt;"Humidity"&lt;/span&gt;

&lt;span class="nt"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;last_co2&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;int&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restore_value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;no&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;initial_value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'0'&lt;/span&gt;

&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;sntp&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;on_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&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="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="nt"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|-&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;int v = id(co2).state;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;return (v &amp;gt; 1000 &amp;amp;&amp;amp; v &amp;gt; id(last_co2));&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rtttl.play&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e'&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;output.turn_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;buzzer&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;5s&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;output.turn_off&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;buzzer&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;globals.set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;last_co2&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!lambda&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;return id(co2).state;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Put your WiFi credentials and other sensitive information in &lt;code&gt;secrets.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="wiring"&gt;Wiring 🔌&lt;/h4&gt;
&lt;p&gt;Connect the components as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SCD41 VCC&lt;/strong&gt; → &lt;strong&gt;3.3V&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SCD41 GND&lt;/strong&gt; → &lt;strong&gt;GND&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SCD41 SDA&lt;/strong&gt; → &lt;strong&gt;ESP8266 D1 (GPIO5)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SCD41 SCL&lt;/strong&gt; → &lt;strong&gt;ESP8266 D2 (GPIO4)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buzzer Positive&lt;/strong&gt; → &lt;strong&gt;ESP8266 D3 (GPIO2)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buzzer Negative&lt;/strong&gt; → &lt;strong&gt;GND&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="uploading-the-code"&gt;Uploading the Code 🚀&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Connect the ESP8266 to your computer via USB.&lt;/li&gt;
&lt;li&gt;Run the following command to upload the firmware:
   &lt;code&gt;sh
   esphome run co2.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Once uploaded, the ESP8266 will connect to your Wi-Fi network and start transmitting CO2 data.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="integration-with-home-assistant"&gt;Integration with Home Assistant 🏡&lt;/h3&gt;
&lt;p&gt;If you have Home Assistant running, simply add the ESPHome integration, and the sensor should automatically appear in your dashboard.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://atlee.ca/posts/co2_history.png"&gt;&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion 🎉&lt;/h3&gt;
&lt;p&gt;With just a few components and ESPHome, you can easily build a smart CO2 monitor for your home or office. This setup allows for real-time monitoring and integration with home automation platforms, providing valuable insights into indoor air quality.&lt;/p&gt;
&lt;p&gt;Building this project not only improved my awareness of indoor air quality but also gave me peace of mind knowing that I have an early warning system when CO2 levels rise too high. If you've ever experienced similar symptoms or want to improve your home’s air quality, I highly recommend giving this project a try!&lt;/p&gt;</description><category>automation</category><category>programming</category><guid>https://atlee.ca/posts/co2-esphome/</guid><pubDate>Sat, 22 Feb 2025 19:26:27 GMT</pubDate></item><item><title>Advent of Code - 2024</title><link>https://atlee.ca/posts/aoc-2024/</link><dc:creator>chris</dc:creator><description>&lt;h2 id="advent-of-code-2024-in-zig"&gt;Advent of Code 2024 in Zig&lt;/h2&gt;
&lt;p&gt;This year, I decided to make things interesting and tackle &lt;a href="https://adventofcode.com/2024"&gt;Advent of Code&lt;/a&gt; using &lt;a href="https://ziglang.org/"&gt;Zig&lt;/a&gt;. Zig is a low-level systems language that aims to be simple, predictable, and efficient. Coming from a Ruby, Python, and Rust background, diving into Zig was a mix of fun, frustration, and some unexpected insights.&lt;/p&gt;
&lt;h3 id="what-i-liked"&gt;What I Liked&lt;/h3&gt;
&lt;h4 id="iteration-feels-good"&gt;Iteration Feels Good 🚀&lt;/h4&gt;
&lt;p&gt;Zig has a few neat iteration features. For example, &lt;code&gt;for&lt;/code&gt; loops allow you to specify an index parameter, which makes enumeration straightforward. Similarly, &lt;code&gt;while&lt;/code&gt; loops integrate nicely with the &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;optional&lt;/code&gt; types, reducing boilerplate. More on that below!&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;printNumbers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;..)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Index: {}, Value: {}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;whileLoopExample&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Iteration: {}, Value: {}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iteratorExample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;anytype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// iter.next() returns an Optional value;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// the loop will terminate when the value is `null`&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// if the value is present, then it's passed in as the captured&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// variable |val| here.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Value: {}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="else-clauses-for-loops"&gt;&lt;code&gt;else&lt;/code&gt; Clauses for loops 🔁&lt;/h4&gt;
&lt;p&gt;A cool feature is the &lt;code&gt;else&lt;/code&gt; clause for &lt;code&gt;for&lt;/code&gt; and &lt;code&gt;while&lt;/code&gt; loops. This lets you handle cases where a loop completes without breaking, which is handy for search algorithms and cleanup logic. Python also supports &lt;code&gt;else&lt;/code&gt; clauses in &lt;code&gt;for&lt;/code&gt; loops, while Ruby does not. This is something I miss about Python when working in Ruby. Python's &lt;code&gt;else&lt;/code&gt; in loops allows for a clean way to handle cases where a loop completes without finding a match, while in Ruby, you often have to rely on &lt;code&gt;find&lt;/code&gt; or &lt;code&gt;detect&lt;/code&gt; instead.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;findValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Value not found!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{});&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="option-and-error-union-types"&gt;Option and Error Union Types ❓⚠️&lt;/h4&gt;
&lt;p&gt;Zig does not have exceptions, instead relying on option and error union types. I really like this style of programming, which you see much more commonly in Rust. Having to explicitly deal with errors rather than letting exceptions bubble up through the stack makes the code a lot easier to reason about. I like that Zig has built in support for working with options and errors.&lt;/p&gt;
&lt;p&gt;Optionals (&lt;code&gt;?T&lt;/code&gt;) help represent values that may be absent, and errors (&lt;code&gt;!T&lt;/code&gt;) are returned instead of thrown.&lt;/p&gt;
&lt;p&gt;You can use &lt;code&gt;orelse&lt;/code&gt; to provide a fallback value for an optional expression, which makes handling possibly &lt;code&gt;null&lt;/code&gt; values straightforward.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;getNumber&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;getNumber&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;orelse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Number: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Optionals can also be used in an &lt;code&gt;if&lt;/code&gt; statement or &lt;code&gt;while&lt;/code&gt; loop to conditionally handle the presence of a value:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;checkOptional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maybe_value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;getNumber&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maybe_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Got a value: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No value found!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{});&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Similarly, &lt;code&gt;catch&lt;/code&gt; is used for handling errors when working with error unions:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mightFail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;anotherFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;handleErrors&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mightFail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Error occurred: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Success: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And &lt;code&gt;try&lt;/code&gt; is used to unwrap the value from an error union, returning any errors to the caller. This is similar to Rust's &lt;a href="https://doc.rust-lang.org/rust-by-example/std/result/question_mark.html"&gt;&lt;code&gt;?&lt;/code&gt; operator&lt;/a&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;handleErrorsWithTry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// If anotherFunction() returns an error, we return it here&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;anotherFunction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Got result: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="labelled-breaks"&gt;Labelled Breaks ⛔&lt;/h4&gt;
&lt;p&gt;Nested loops are usually annoying, but Zig’s labelled &lt;code&gt;break&lt;/code&gt; statements make them way easier to manage. No more tracking weird flags just to escape a loop early.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nestedLoop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{} {}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="pointers-yes-actually"&gt;Pointers (Yes, Actually) ➡️&lt;/h4&gt;
&lt;p&gt;Pointers generally strike fear into the heart of programmers, but in Zig they feel well-structured and easy to use. The explicit handling makes memory management clearer and less error-prone. In Rust, you have to fight against the borrow checker a lot, especially when working with tree structures—which are very common in Advent of Code! Being able to use pointers makes writing these kinds of data structures far less cumbersome... as long as you don't mind debugging a few segfaults.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pointerExample&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Value: {}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="memory-management"&gt;Memory Management 🏗️&lt;/h4&gt;
&lt;p&gt;Building on the advantages of pointers, I appreciated having more direct control over memory allocations. Zig’s explicit allocator model and the &lt;code&gt;defer&lt;/code&gt; keyword make resource management predictable and efficient. Being able to choose and switch allocators as needed, especially leveraging an arena allocator when performance and cleanup efficiency matter, is a huge plus. It’s refreshing to have fine-grained memory control without excessive boilerplate.&lt;/p&gt;
&lt;p&gt;Here's an example of using an arena allocator for efficient memory management:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;@import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"std"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allocator&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Allocator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ArrayList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allocator&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deinit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;arena&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ArenaAllocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_allocator&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;arena&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deinit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;allocator&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;arena&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allocator&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="annoyances"&gt;Annoyances 😬&lt;/h3&gt;
&lt;p&gt;Zig isn't perfect (yet? 😛). A few things that frustrated me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Verbose Debug Printing:&lt;/strong&gt; Zig doesn’t have varargs, so you have to use struct literals when passing arguments to &lt;code&gt;std.debug.print&lt;/code&gt;. While this makes sense, it can feel tedious to wrap arguments in an anonymous struct &lt;code&gt;.{ ... }&lt;/code&gt; all the time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lack of Functional Programming Constructs:&lt;/strong&gt; Coming from Ruby and Rust, I missed having functional constructs like &lt;code&gt;map&lt;/code&gt; and &lt;code&gt;filter&lt;/code&gt;. In Zig, accomplishing similar transformations often requires writing explicit loops, fighting with types, which made writing data transformation code more cumbersome.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cryptic Compiler Errors:&lt;/strong&gt; Some compiler errors, particularly related to &lt;code&gt;comptime&lt;/code&gt; operations like string formatting, can be difficult to trace. The error messages don’t always point to the exact issue in the source code, which makes debugging trickier. Hopefully, this improves in future versions of Zig.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="final-thoughts"&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Despite the quirks, using Zig for Advent of Code was a fun challenge. It forced me to think differently about problem-solving and get my hands dirty with lower-level programming again. The explicit memory management and structured error handling are great, but I definitely missed the expressiveness of Ruby and Rust.&lt;/p&gt;
&lt;p&gt;If you’re curious about my solutions, you can check them out on &lt;a href="https://github.com/catlee/adventofcode/tree/master/2024/zig"&gt;GitHub&lt;/a&gt;. Zig is an interesting language, and while I won’t be using it for everything, it’s definitely worth exploring!&lt;/p&gt;</description><category>aoc</category><category>programming</category><category>zig</category><guid>https://atlee.ca/posts/aoc-2024/</guid><pubDate>Sun, 16 Feb 2025 00:20:34 GMT</pubDate></item><item><title>Packing bits with Rust &amp; Ruby</title><link>https://atlee.ca/posts/packing-bits-with-rust-ruby/</link><dc:creator>chris</dc:creator><description>&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/packing-bits-with-rust-ruby/#the-missing-c-of-chd"&gt;The missing C of CHD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/packing-bits-with-rust-ruby/#packing-seeds"&gt;Packing seeds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/packing-bits-with-rust-ruby/#bridging-the-gap-between-rust-ruby"&gt;Bridging the gap between Rust &amp;amp; Ruby&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/packing-bits-with-rust-ruby/#bitpacking-gem"&gt;BitPacking gem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="the-missing-c-of-chd"&gt;The missing C of CHD&lt;/h2&gt;
&lt;p&gt;One element of the &lt;a href="https://en.wikipedia.org/wiki/Perfect_hash_function#Pseudocode"&gt;CHD&lt;/a&gt; (compress-hash-displace)
algorithm that I didn't implement in my &lt;a href="https://atlee.ca/posts/faster-hash-lookups"&gt;previous post&lt;/a&gt; was the "compress" part.&lt;/p&gt;
&lt;p&gt;This algorithm generates an auxiliary table of seeds that are used to prevent hash collisions in the data set. These
seeds need to be encoded somehow and transmitted along with the rest of the data in order to perform lookups later on.
The number of seeds (called &lt;code&gt;r&lt;/code&gt; in the algorithm) here is usually proportional to the number of elements in the input.
Having a larger &lt;code&gt;r&lt;/code&gt; means that it's easier to compute seeds that avoid collisions, and therefore faster to compute the
perfect hash. Reducing &lt;code&gt;r&lt;/code&gt; results in a more compact data structure at the expense of more compute up-front.&lt;/p&gt;
&lt;h2 id="packing-seeds"&gt;Packing seeds&lt;/h2&gt;
&lt;p&gt;Seeds are generally tried starting from 0, and typically don't end up being very large. Encoding these values as a
basic array of 8/16/32-bit integers is a waste of space.&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://atlee.ca/posts/bitpacking-zeros.png" alt="lots of zeros"&gt;
&lt;/p&gt;

&lt;p&gt;I wanted to improve on my implementation of efficient encoding
of hashes by doing some simple bit packing of the seeds.&lt;/p&gt;
&lt;p&gt;The basic idea is that for a set of integers, we find the maximum value, and therefore the maximum number of bits (&lt;code&gt;b&lt;/code&gt;) needed
to represent that value. We can then encode all the integers using &lt;code&gt;b&lt;/code&gt; bits instead of a fixed number of bits.&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://atlee.ca/posts/bitpacking.png" alt="less zeros"&gt;
&lt;/p&gt;

&lt;p&gt;There's a Rust crate &lt;a href="https://github.com/quickwit-oss/bitpacking"&gt;&lt;code&gt;bitpacking&lt;/code&gt;&lt;/a&gt; that does exactly this! And it runs super
duper fast, assuming that you can arrange your data into
groups of 32/128/256 integers. The API is really simple to use as well:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bitpacking&lt;/span&gt;::&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BitPacker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BitPacker4x&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;packer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BitPacker4x&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num_bits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;packer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_bits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="k"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BitPacker4x&lt;/span&gt;::&lt;span class="n"&gt;BLOCK_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;packer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_mut_slice&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num_bits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="fm"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Compressed data: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="bridging-the-gap-between-rust-ruby"&gt;Bridging the gap between Rust &amp;amp; Ruby&lt;/h2&gt;
&lt;p&gt;I wanted to use this from Ruby code though...time to bust out &lt;a href="https://github.com/matsadler/magnus"&gt;&lt;code&gt;magnus&lt;/code&gt;&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Magnus is a crate which makes it really easy to write Ruby extensions using Rust. It takes care of most of the heavy
lifting of converting to/from Ruby &amp;amp; Rust types.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#[magnus::wrap(class=&lt;/span&gt;&lt;span class="s"&gt;"BitPacking::BitPacker4x"&lt;/span&gt;&lt;span class="cp"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;BitPacker4x&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bitpacking&lt;/span&gt;::&lt;span class="n"&gt;BitPacker4x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BitPacker4x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;ruby&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;Ruby&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;rb_self&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;decompressed&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;num_bits&lt;/span&gt;: &lt;span class="kt"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;RString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="k"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;BLOCK_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rb_self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// refers to underlying bitpacking::BitPacker4x struct&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;decompressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_mut_slice&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num_bits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;ruby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_slice&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This lets me write Ruby code like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;packer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;BitPacking&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BitPacker4x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;num_bits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;packer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_bits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;packer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num_bits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here we have these 128 integers represented in 48 bytes, or 3 bits per integer.&lt;/p&gt;
&lt;h2 id="bitpacking-gem"&gt;BitPacking gem&lt;/h2&gt;
&lt;p&gt;I've packaged this up into the &lt;a href="https://github.com/catlee/bitpacking"&gt;&lt;code&gt;bitpacking&lt;/code&gt;&lt;/a&gt; gem.&lt;/p&gt;
&lt;p&gt;I hope you find this useful!&lt;/p&gt;</description><category>programming</category><category>ruby</category><category>rust</category><category>yaks</category><guid>https://atlee.ca/posts/packing-bits-with-rust-ruby/</guid><pubDate>Fri, 18 Oct 2024 01:24:39 GMT</pubDate></item><item><title>Efficient Hash Lookups: Adventures with Benchmarks!</title><link>https://atlee.ca/posts/faster-hash-lookups/</link><dc:creator>chris</dc:creator><description>&lt;p&gt;Efficient data retrieval is crucial for high performance applications. Whether
you're configuring a complex system or handling large datasets, the speed of
hash lookups can significantly impact performance. In this post, we'll explore
various methods to optimize hash lookups and benchmark their performance.&lt;/p&gt;
&lt;p&gt;You can find my benchmarking code available &lt;a href="https://github.com/catlee/faster-hash-lookups"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/faster-hash-lookups/#the-challenge"&gt;The Challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/faster-hash-lookups/#awesome-whats-the-problem"&gt;Awesome, what's the problem?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/faster-hash-lookups/#how-can-we-make-this-better"&gt;How can we make this better?&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/faster-hash-lookups/#messagepack"&gt;MessagePack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/faster-hash-lookups/#protobufs"&gt;Protobufs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/faster-hash-lookups/#encode-as-an-array"&gt;Encode as an array&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/faster-hash-lookups/#perfect-hashes"&gt;Perfect hashes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atlee.ca/posts/faster-hash-lookups/#summary"&gt;Summary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="the-challenge"&gt;The Challenge&lt;/h2&gt;
&lt;p&gt;Let's say that you have a &lt;a href="https://ruby-doc.org/3.3.5/Hash.html"&gt;hash&lt;/a&gt; (aka
&lt;a href="https://docs.python.org/3/library/stdtypes.html#mapping-types-dict"&gt;dictionary&lt;/a&gt;
aka
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"&gt;map&lt;/a&gt;
aka &lt;a href="https://en.wikipedia.org/wiki/Associative_array"&gt;associative array&lt;/a&gt;;
something with key/value
pairs) representing configuration for your system.&lt;/p&gt;
&lt;p&gt;At runtime, let's assume that your application only needs access to a small
number of these configuration items to handle a given request.&lt;/p&gt;
&lt;p&gt;Let's also say that this hash can get pretty large; some users have very complex
configurations.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"setting_1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s2"&gt;"value 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"setting_2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s2"&gt;"value 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"setting_10000"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value 10000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A common way of storing this configuration is in a
&lt;a href="https://www.json.org/json-en.html"&gt;JSON&lt;/a&gt; file. JSON is really simple to use for this
type of problem. Just use something like &lt;code&gt;JSON.parse&lt;/code&gt; to parse your file, and do a lookup
for whatever key you want:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;config_data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"config.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"setting_500"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "value 500"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="awesome-whats-the-problem"&gt;Awesome, what's the problem?&lt;/h2&gt;
&lt;p&gt;Assuming that we just want to fetch a small number of items from this
configuration, there are a few problems here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We have to load the entire file into memory&lt;/li&gt;
&lt;li&gt;We then have to parse the entire file to get our hash object. This involves
   inserting every item in configuration into a new hash object, which is an &lt;em&gt;O(n)&lt;/em&gt; operation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If our configuration is quite large, then the parsing &amp;amp; hash construction time
will be far greater than our lookup time.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;ruby&lt;span class="w"&gt; &lt;/span&gt;benchmark.rb&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;json&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;
&lt;span class="go"&gt;Calculating -------------------------------------&lt;/span&gt;
&lt;span class="go"&gt;                load    829.185 (±30.0%) i/s    (1.21 ms/i)&lt;/span&gt;
&lt;span class="go"&gt;               parse     24.779 (± 8.1%) i/s   (40.36 ms/i)&lt;/span&gt;
&lt;span class="go"&gt;             get_key     4.242M (± 3.7%) i/s  (235.72 ns/i)&lt;/span&gt;
&lt;span class="go"&gt;  load/parse/get_key     22.561 (± 8.9%) i/s   (44.32 ms/i)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;(I've removed some superfluous information from the output snippets here in the
interests of clarity; the number I'm paying attention to is the time per
iteration represented in the last column.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Lookups into the hash, once created, are very fast: about 200ns per lookup.&lt;/p&gt;
&lt;p&gt;However, for 100,000 entries, just parsing the data and creating the hash takes about
40ms on my system.&lt;/p&gt;
&lt;p&gt;In fact, &lt;strong&gt;the hash construction time is the fundamental issue&lt;/strong&gt; to overcome
when trying to speed up our overall time here. Simply creating a hash from the
data takes about 20ms on my system:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;ruby&lt;span class="w"&gt; &lt;/span&gt;benchmark-to_h.rb&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;output/array.json
&lt;span class="go"&gt;Calculating -------------------------------------&lt;/span&gt;
&lt;span class="go"&gt;                to_h     37.764 (±18.5%) i/s   (26.48 ms/i)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It makes sense when you think about it. Before we can get our hash object containing
our configuration, we need to iterate through each key/value pair and insert it
into the hash object. Only then can we do a lookup for the key we're interested
in.&lt;/p&gt;
&lt;h2 id="how-can-we-make-this-better"&gt;How can we make this better?&lt;/h2&gt;
&lt;p&gt;A bunch of different ideas come to mind:&lt;/p&gt;
&lt;p&gt;We can try different ways to encode the data. For example: using
&lt;a href="https://msgpack.org/index.html"&gt;MessagePack&lt;/a&gt;, &lt;a href="https://protobuf.dev/"&gt;protobufs&lt;/a&gt;,
or &lt;a href="https://capnproto.org/"&gt;capnproto&lt;/a&gt;. Each of these use more efficient ways to
serialize/de-serialize the data, speeding up the parsing time.&lt;/p&gt;
&lt;p&gt;Another idea is to encode our data as a sorted &lt;a href="https://atlee.ca/posts/faster-hash-lookups/#encode-as-an-array"&gt;list&lt;/a&gt; of
key/value pairs instead of as a hash, and use a binary search to lookup the key
that we want.&lt;/p&gt;
&lt;p&gt;Or, we can try and use &lt;a href="https://atlee.ca/posts/faster-hash-lookups/#perfect-hashes"&gt;perfect hashes&lt;/a&gt;; here we would encode
our data as a list of key/value pairs as above, but augmented with additional
information that makes it possible to treat the array as a pre-computed hash
table.&lt;/p&gt;
&lt;p&gt;Other ideas: sqlite, dbm/lmdb. These benefit from encoding the index within the
file itself, so we don't need to reconstruct a hash table/index again when loading the data. Storing
more structured data in these is more challenging though, and lookups in these
types of data stores are typically &lt;em&gt;O(log N)&lt;/em&gt; instead of &lt;em&gt;O(1)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="messagepack"&gt;MessagePack&lt;/h3&gt;
&lt;p&gt;MessagePack is an easy thing to try first since it's almost a drop-in
replacement for JSON in our use case.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;ruby&lt;span class="w"&gt; &lt;/span&gt;benchmark.rb&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;msgpack&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;
&lt;span class="go"&gt;Calculating -------------------------------------&lt;/span&gt;
&lt;span class="go"&gt;                load    679.466 (± 5.3%) i/s    (1.47 ms/i)&lt;/span&gt;
&lt;span class="go"&gt;               parse     27.958 (± 3.6%) i/s   (35.77 ms/i)&lt;/span&gt;
&lt;span class="go"&gt;             get_key     4.692M (± 3.4%) i/s  (213.13 ns/i)&lt;/span&gt;
&lt;span class="go"&gt;  load/parse/get_key     27.031 (± 3.7%) i/s   (36.99 ms/i)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Well, we've improved the parsing time by about about 5ms, which is a nice
improvement, but still pretty slow overall. Again, we have that large hash
construction time to overcome, which MessagePack doesn't help us with.&lt;/p&gt;
&lt;h3 id="protobufs"&gt;Protobufs&lt;/h3&gt;
&lt;p&gt;To experiment with Protobufs, I've created a simple definition using the native &lt;code&gt;map&lt;/code&gt; type:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;syntax&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"proto3"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;PBHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The results here are surprising:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;ruby&lt;span class="w"&gt; &lt;/span&gt;benchmark.rb&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;pb&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;
&lt;span class="go"&gt;Calculating -------------------------------------&lt;/span&gt;
&lt;span class="go"&gt;                load      1.311k (±35.9%) i/s  (762.93 μs/i)&lt;/span&gt;
&lt;span class="go"&gt;               parse      21.496 (± 4.7%) i/s   (46.52 ms/i) # !!&lt;/span&gt;
&lt;span class="go"&gt;             get_key      3.098M (± 3.2%) i/s  (322.75 ns/i)&lt;/span&gt;
&lt;span class="go"&gt;  load/parse/get_key      20.707 (± 4.8%) i/s   (48.29 ms/i)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It looks like parsing protobuf maps are significantly slower than parsing the
equivalent JSON or msgpack objects.&lt;/p&gt;
&lt;h3 id="encode-as-an-array"&gt;Encode as an array&lt;/h3&gt;
&lt;p&gt;Let's see what happens if we encode our data as an array.&lt;/p&gt;
&lt;p&gt;This may improve parsing time, since we don't need to construct a large hash
object in memory. The trade-off is that lookup times become &lt;em&gt;O(log n)&lt;/em&gt; instead of
&lt;em&gt;O(1)&lt;/em&gt;, &lt;strong&gt;assuming we can sort our input&lt;/strong&gt;. As a first approximation, we can
compare times to load &amp;amp; parse the array.&lt;/p&gt;
&lt;p&gt;Using JSON I see marginal improvement to load &amp;amp; parse the data (~30ms), but
using Protobufs we can load &amp;amp; parse the data in about 6ms. Unfortunately I
couldn't get good measurements for the full load/parse/get_key with protobufs;
it seems like doing a binary search for elements in the protobuf arrays is
relatively slow in Ruby. Fetching individual items is fast.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="go"&gt;ruby -Iproto benchmark.rb -f pb -s array&lt;/span&gt;
&lt;span class="go"&gt;Calculating -------------------------------------&lt;/span&gt;
&lt;span class="go"&gt;                load     1.641k (±10.7%) i/s  (609.32 μs/i)&lt;/span&gt;
&lt;span class="go"&gt;               parse    163.886 (± 3.1%) i/s    (6.10 ms/i)&lt;/span&gt;
&lt;span class="go"&gt;             get_key     27.325 (±11.0%) i/s   (36.60 ms/i)&lt;/span&gt;
&lt;span class="go"&gt;  load/parse/get_key      3.000 (±33.3%) i/s  (333.32 ms/i) # ???&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="perfect-hashes"&gt;Perfect hashes&lt;/h3&gt;
&lt;p&gt;Maybe we can get the best of both worlds by using a &lt;a href="https://en.wikipedia.org/wiki/Perfect_hash_function"&gt;perfect
hash&lt;/a&gt; to order our data in
a particular way to make it cheap to find?&lt;/p&gt;
&lt;p&gt;There are several ways of constructing a perfect hash; the
&lt;a href="https://cmph.sourceforge.net/papers/esa09.pdf"&gt;CHD&lt;/a&gt; algorithm is &lt;a href="https://gist.github.com/pervognsen/b21f6dd13f4bcb4ff2123f0d78fcfd17"&gt;fairly easy to
implement&lt;/a&gt; (without the "compress" part of compress-hash-displace)
, and can compute a minimal perfect hash function in &lt;em&gt;O(n)&lt;/em&gt; time.&lt;/p&gt;
&lt;p&gt;The way it works is that we compute a secondary array of seeds that are used to
prevent collisions between keys in the data set. To find the index of an element
by its key, we compute:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Find index for `key`&lt;/span&gt;
&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seeds&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hashfunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seeds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hashfunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The results of using CHD with Protobufs are very good (using cityhash as the
hashing function):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;ruby&lt;span class="w"&gt; &lt;/span&gt;benchmark.rb&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;pb&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;chd
&lt;span class="go"&gt;Calculating -------------------------------------&lt;/span&gt;
&lt;span class="go"&gt;                load     1.920k (± 7.0%) i/s  (520.85 μs/i)&lt;/span&gt;
&lt;span class="go"&gt;               parse    173.076 (± 4.0%) i/s    (5.78 ms/i)&lt;/span&gt;
&lt;span class="go"&gt;             get_key   491.510k (± 1.9%) i/s    (2.03 μs/i)&lt;/span&gt;
&lt;span class="go"&gt;  load/parse/get_key    154.828 (± 1.9%) i/s    (6.46 ms/i)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;Wow, that was a lot! What did we learn?&lt;/p&gt;
&lt;p&gt;Benchmarks are hard! I wouldn't take any results here as absolute proof that X
is better or worse than Y. Your data and use cases are most likely very different.&lt;/p&gt;
&lt;p&gt;Using perfect hashes or binary search in an array of key/value pairs look like
they're much faster in general than simple lookups into a hash object. The time
to construct the hash object plays such a large part in the overall timings.&lt;/p&gt;
&lt;p&gt;I'm surprised at the performance of protobufs in generally. They're much slower
than I expected, in everything except the perfect hash (CHD) case. It makes me
wonder if something is wrong with my implementation, or &lt;a href="https://rubygems.org/gems/google-protobuf/versions/4.28.2"&gt;Ruby's protobuf
gem&lt;/a&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$  &lt;/span&gt;ruby&lt;span class="w"&gt; &lt;/span&gt;benchmark-final.rb
&lt;span class="go"&gt;Comparison:&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key pb chd:        124.3 i/s&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key msgpack array: 50.2 i/s - 2.48x  slower&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key msgpack chd:   41.1 i/s - 3.02x  slower&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key json array:    37.3 i/s - 3.33x  slower&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key json chd:      27.9 i/s - 4.46x  slower&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key msgpack hash:  23.8 i/s - 5.22x  slower&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key json hash:     20.7 i/s - 6.00x  slower&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key pb hash:       19.4 i/s - 6.40x  slower&lt;/span&gt;
&lt;span class="go"&gt;load/parse/get_key pb array:      3.1 i/s - 40.07x  slower&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To follow up, I'd like to explore using &lt;a href="https://capnproto.org/"&gt;capnproto&lt;/a&gt;,
and running similar benchmarks in other languages to see if the protobuf
performance improves.&lt;/p&gt;</description><category>performance</category><category>programming</category><category>ruby</category><guid>https://atlee.ca/posts/faster-hash-lookups/</guid><pubDate>Wed, 16 Oct 2024 00:20:34 GMT</pubDate></item><item><title>Extrakto!</title><link>https://atlee.ca/posts/extrakto/</link><dc:creator>chris</dc:creator><description>&lt;p&gt;One of my favourite tmux plugins is &lt;a href="https://github.com/laktak/extrakto"&gt;extrakto&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's this amazing little plugin that lets you insert text by fuzzy finding within the current tmux session.&lt;/p&gt;
&lt;p&gt;Why is this useful?&lt;/p&gt;
&lt;p&gt;Think of how often you copy/paste some line of text from the terminal in order to pass it into another command. For example, the output of &lt;code&gt;git status&lt;/code&gt;, or perhaps your build or test failed and you want to open that file in your editor (&lt;a href="https://atlee.ca/categories/vim/"&gt;nvim&lt;/a&gt; of course!).&lt;/p&gt;
&lt;p&gt;Instead of taking your fingers off your keyboard, &lt;code&gt;extrakto&lt;/code&gt; lets you quickly find the text you want and add it at the current cursor position.&lt;/p&gt;
&lt;p&gt;Using it is simple: just type your prefix (Ctrl-b for me), then tab, type your fuzzy search, and hit tab again to insert the text.&lt;/p&gt;
&lt;video width="800" controls&gt;
    &lt;source src="https://atlee.ca/posts/extrakto.webm"&gt;
&lt;/source&gt;&lt;/video&gt;</description><category>shell</category><category>tmux</category><guid>https://atlee.ca/posts/extrakto/</guid><pubDate>Sat, 02 Mar 2024 01:50:17 GMT</pubDate></item><item><title>Pull Diagnostic Support for Neovim</title><link>https://atlee.ca/posts/pull-diagnostic-support-for-neovim/</link><dc:creator>chris</dc:creator><description>&lt;p&gt;I've used &lt;a href="https://www.vim.org/"&gt;&lt;code&gt;vim&lt;/code&gt;&lt;/a&gt; as my primary text / code editing tool for...literally decades. A few years ago, I tried out &lt;a href="https://neovim.io/"&gt;&lt;code&gt;neovim&lt;/code&gt;&lt;/a&gt;, hoping to take advantage of more advanced developer oriented features like &lt;a href="https://tree-sitter.github.io/tree-sitter/"&gt;&lt;code&gt;treesitter&lt;/code&gt;&lt;/a&gt; and native &lt;a href="https://en.wikipedia.org/wiki/Language_Server_Protocol"&gt;&lt;code&gt;LSP&lt;/code&gt;&lt;/a&gt; support.&lt;/p&gt;
&lt;p&gt;At &lt;a href="https://atlee.ca/categories/shopify/"&gt;Shopify&lt;/a&gt;, I do a lot of work in Ruby. Our incredible &lt;a href="https://shopify.engineering/improving-the-developer-experience-with-ruby-lsp"&gt;developer experience&lt;/a&gt; team maintain &lt;a href="https://github.com/Shopify/ruby-lsp"&gt;&lt;code&gt;ruby-lsp&lt;/code&gt;&lt;/a&gt;, a language server for the Ruby language.&lt;/p&gt;
&lt;p&gt;Of course, I wanted to make sure that using it in neovim was a great experience!&lt;/p&gt;
&lt;p&gt;LSPs can do many things: auto completion, enabling jumping to function or type definitions, showing function documentation, code formatting, and displaying code linting results.&lt;/p&gt;
&lt;p&gt;In this particular instance, I wanted to take advantage of &lt;code&gt;ruby-lsp&lt;/code&gt;'s "diagnostics" support, which provides real-time feedback about syntax or linting errors (via &lt;a href="https://rubocop.org/"&gt;&lt;code&gt;rubocop&lt;/code&gt;&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://atlee.ca/posts/ruby_diags.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;brief aside: I find the nomenclature here is a bit awkward. LSP is an abbreviation for "Language Server Protocol". neovim implements a client which implements the LSP, which connects to one or more servers with also implement the protocol. Commonly a server which implements the LSP protocol is referred to as an "LSP server" or "LSP." I'll do that here.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="push-vs-pull"&gt;Push vs pull&lt;/h2&gt;
&lt;p&gt;Unfortunately for me, neovim before 0.10 only supports "push" diagnostics, and &lt;code&gt;ruby-lsp&lt;/code&gt; only supports "pull" diagnostics.&lt;/p&gt;
&lt;p&gt;"Push" diagnostics are where the LSP pushes diagnostic information about your source code to the client (neovim in this case). The challenge with push diagnostics is that it's difficult for the LSP to know when to generate and send the diagnostics. It has no way to coordinate with other LSPs, and doesn't really how the user is interacting with the editor, aside from receiving events when the sourcecode has been changed. This becomes a problem when many changes to a file are made; how does the LSP know when to generate and push the diagnostics? Should it do it on every change? Wait for a bit after the last change is made?&lt;/p&gt;
&lt;p&gt;These challenges are why the language server protocol was updated to add support for "pull" diagnostics. In this model the client (neovim) requests diagnostics from the LSP server, which is much more efficient since the client (neovim) can make much better decisions about when to perform sourcecode diagnostics.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;more technically, "push" diagnostics are implemented via the &lt;a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics"&gt;&lt;code&gt;textDocument/publishDiagnostics&lt;/code&gt;&lt;/a&gt; method. "pull" diagnostics are implemented via the &lt;a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics"&gt;&lt;code&gt;textDocument/diagnostic&lt;/code&gt;&lt;/a&gt; method&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="pull-support-in-neovim-open-source-ftw"&gt;Pull support in neovim (open-source FTW!)&lt;/h2&gt;
&lt;p&gt;Last year I spent some time &lt;a href="https://github.com/neovim/neovim/pull/24128"&gt;adding support&lt;/a&gt; for pull diagnostics to neovim 0.10. I learned a lot about neovim, LSP, and lua in the process!&lt;/p&gt;
&lt;p&gt;To me, this really highlights the benefits of open-source: there was something in my toolbox that I wasn't happy with, and I am empowered (and encouraged!) to contribute and make it better for everyone! Of course, I relied heavily on more experienced contributors to the project, and, of course, my initial implementation required &lt;a href="https://github.com/neovim/neovim/pull/24411"&gt;some&lt;/a&gt; &lt;a href="https://github.com/neovim/neovim/issues/24456"&gt;follow&lt;/a&gt;-&lt;a href="https://github.com/neovim/neovim/pull/24469"&gt;ups&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="my-first-neovim-plugin"&gt;My first neovim plugin&lt;/h2&gt;
&lt;p&gt;However, neovim 0.10 hasn't been released yet, and so I also wanted to make sure that I could use &lt;code&gt;ruby-lsp&lt;/code&gt; with stable (0.9.x) versions of neovim.&lt;/p&gt;
&lt;p&gt;So I created my very first neovim plugin: &lt;a href="https://github.com/catlee/pull_diags.nvim"&gt;https://github.com/catlee/pull_diags.nvim&lt;/a&gt;. This enables "pull" diagnostics for neovim prior to 0.10. It's not quite as efficient as the support built into 0.10, but it works for my day to day development. If it doesn't work for you, please let me know!&lt;/p&gt;
&lt;p&gt;Installing it is quite simple; for example, with &lt;a href="https://github.com/folke/lazy.nvim/tree/main"&gt;Lazy.nvim&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"catlee/pull_diags.nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LspAttach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;&lt;img alt="" src="https://atlee.ca/posts/sharpen.gif"&gt;&lt;/p&gt;
&lt;p&gt;As a professional software engineer, I think it's important to invest in the tools that I use to get my work done. Concretely, this means that I need (and want!) to spend time learning what new tools and techniques are out there now, and to make sure to &lt;a href="https://blog.codinghorror.com/sharpening-the-saw/"&gt;sharpen my tools.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this case, I wanted to take advantage of a new technology (&lt;code&gt;LSP&lt;/code&gt; &amp;amp; &lt;code&gt;ruby-lsp&lt;/code&gt;) in my editor of choice (&lt;code&gt;neovim&lt;/code&gt;). Since these tools are all open-source, I'm empowered to change them to fit my needs.&lt;/p&gt;
&lt;p&gt;In other words, as professionals, we shouldn't accept drawbacks or limitations in our tools: we should fix them! It's just another piece of software after all!&lt;/p&gt;</description><category>lsp</category><category>lua</category><category>neovim</category><category>shopify</category><category>vim</category><category>work</category><guid>https://atlee.ca/posts/pull-diagnostic-support-for-neovim/</guid><pubDate>Tue, 06 Feb 2024 15:31:10 GMT</pubDate></item></channel></rss>