<?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/" xmlns:wc="https://wilhelm.codes/ns/feed/1.0"><channel><title>Wilhelm Codes · Go</title><link>https://wilhelm.codes/tags/go/</link><description>Slinger of nibbles, bits and bytes. Over 25 years of professional experience as a software engineer. Love making glowing rectangles go "beep-boop".</description><generator>Hugo</generator><language>en-US</language><managingEditor>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</managingEditor><webMaster>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</webMaster><lastBuildDate>Mon, 24 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://wilhelm.codes/tags/go/index.xml" rel="self" type="application/rss+xml"/><item><title>Glazier Release: v0.1.5</title><link>https://wilhelm.codes/blog/glazier-release-015/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/glazier-release-015/</guid><category>go</category><category>hcl</category><category>tmux</category><category>glazier</category><wc:kind>post</wc:kind><description>Today, I minted and tagged Glazier v0.1.5. Immediately after, realised I never announced v0.1.4, the release with all the actual features in it. So, Happy Birthday, you get a twofer.</description><content:encoded><![CDATA[<p>Today, I minted and tagged <a href="https://github.com/wilhelm-murdoch/glazier">Glazier</a> <code>v0.1.5</code>. Immediately after, realised I never announced <code>v0.1.4</code>, the release with all the actual features in it. So, Happy Birthday, you get a twofer.</p>
<h2 id="lets-get-v015-out-of-the-way-first">
  <a class="heading-link" href="#lets-get-v015-out-of-the-way-first">Let&rsquo;s get <code>v0.1.5</code> out of the way first.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>There&rsquo;s not much to see here, honestly. This is a routine housekeeping and dependency bump release. The only change worthy of a mention is is a bump of <code>golang.org/x/text</code> to address <a href="https://pkg.go.dev/vuln/GO-2026-5970">GO-2026-5970</a>. This is the sort of thing you patch promptly even when the practical exposure for a tmux wrapper is <code>nil</code> ( ha-ha-ha ). Alongside that, the Go toolchain moved up to <code>1.26.4</code> and <code>testify</code>, <code>urfave/cli</code> and a stack of GitHub Actions all got their routine nudges courtesy of Dependabot. Hey, thanks little buddy!</p>
<p>Download the latest release, replace your current binary and you should be good to go.</p>
<h2 id="v014-is-where-the-features-live">
  <a class="heading-link" href="#v014-is-where-the-features-live"><code>v0.1.4</code> is where the features live.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<div class="callout callout-info">
  Before we get into it I should point out this release introduces a few breaking changes to the spec that should be easy enough to resolve thanks to Glazier&rsquo;s awesome built-in error reporting.
</div>

<p>Back in July I wrote about <a href="https://wilhelm.codes/blog/i-couldnt-talk-myself-out-of-typed-variables/">adding typed variable blocks to Glazier</a>. That work shipped on July 17th as <code>v0.1.4</code>.</p>
<p>The short recap, if you didn&rsquo;t read that post is that profiles can now declare the inputs they accept as first-class <code>variable</code> blocks. Just like, you guessed it, Terraform. Each block can carry a primitive type (<code>string</code>, <code>number</code> or <code>bool</code>), a <code>default</code> and a <code>description</code>. No default means the variable is required. Pass a <code>--var</code> the profile never declared and you get a proper located error.</p>
<p>Values are read through the <code>var.</code> namespace and only that namespace:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;district&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  description</span> <span class="o">=</span> <span class="s2">&#34;the district the gig is themed after&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  type</span>        <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl"><span class="n">  default</span>     <span class="o">=</span> <span class="s2">&#34;watson&#34;</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;fixer&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  type</span> <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;gig-${var.district}&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;${var.fixer}-ops&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;echo ${var.fixer} has the next job&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var <span class="nv">fixer</span><span class="o">=</span>wakako
</span></span><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var <span class="nv">district</span><span class="o">=</span>arasaka --var <span class="nv">fixer</span><span class="o">=</span>wakako
</span></span></code></pre></div>
</div>
<p>That post covers the design reasoning, the type coercion and the breaking namespace migration in detail. So, no need to reshash it all here. What it <em>doesn&rsquo;t</em> cover is everything else that snuck into the release after I hit publish button.</p>
<h2 id="support-for-locals">
  <a class="heading-link" href="#support-for-locals">Support for locals.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Variables are <em>inputs</em>. They cross the boundary from the outside world into your profile via <code>--var</code> flags, var files or defaults. But the moment I had proper variables I ran into their natural companion problem; <em>derived</em> values. The thing you compute from an input once and then want to reuse in five places without copy-pasting the same expression five times.</p>
<p>Terraform solves this with <code>locals</code> and, because Glazier remains an unapologetic love letter to Terraform&rsquo;s parser, it does as well:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;district&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  default</span> <span class="o">=</span> <span class="s2">&#34;night city&#34;</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">locals</span> {
</span></span><span class="line"><span class="cl"><span class="n">  slug</span>    <span class="o">=</span> <span class="k">lower</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">var</span><span class="p">.</span><span class="k">district</span><span class="p">,</span> <span class="s2">&#34; &#34;, &#34;-&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">  session</span> <span class="o">=</span> <span class="s2">&#34;gig-${local.slug}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  logfile</span> <span class="o">=</span> <span class="s2">&#34;${path.pwd}/logs/${local.slug}.log&#34;</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="k">local</span><span class="p">.</span><span class="k">session</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;${local.slug}-ops&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;tail -f ${local.logfile}&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;editor&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;nvim&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>You declare named values in a <code>locals</code> block at the top level and read them back through <code>local.&lt;name&gt;</code>. Plural block and a singular reference is exactly how Terraform does it.</p>
<p>A local can reference the <code>var.*</code>, <code>env.*</code>, <code>path.*</code> namespaces, the whole function library and, crucially, <em>other locals</em>. In any order. This works fine:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">locals</span> {
</span></span><span class="line"><span class="cl"><span class="n">  session</span> <span class="o">=</span> <span class="s2">&#34;gig-${local.slug}&#34;</span><span class="c1">                    # references slug...
</span></span></span><span class="line"><span class="cl"><span class="n">  slug</span>    <span class="o">=</span> <span class="k">lower</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">var</span><span class="p">.</span><span class="k">district</span><span class="p">,</span> <span class="s2">&#34; &#34;, &#34;-&#34;</span><span class="p">))</span><span class="c1"> # ...declared after it
</span></span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Under the hood resolution just keeps iterating, evaluating whatever it can each pass until the end. Whatever&rsquo;s left over then reports its <em>real</em> evaluation diagnostics, so a genuine fat-fingered mistake surfaces as the actual error rather than being masked by ordering machinery. If you accidentally declare the same local twice you&rsquo;ll get told exactly where both declarations live.</p>
<p>The one thing you can&rsquo;t do with a local is set it from the command line and this is by design. Variables are the contract with the outside world; locals are private. If a value should be overridable, it&rsquo;s a variable. If it&rsquo;s just an expression you&rsquo;re tired of repeating, it&rsquo;s a local. Having the language enforce that distinction is one of those small things that keeps a profile clean as it grows.</p>
<p>They also pair nicely with the new <code>random</code> function and HCL&rsquo;s inline comprehensions, both of which landed in this release too:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">locals</span> {
</span></span><span class="line"><span class="cl"><span class="n">  editors</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;nvim&#34;, &#34;hx&#34;, &#34;vim&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  greeting</span> <span class="o">=</span> <span class="k">random</span><span class="p">([</span><span class="k">for</span> <span class="k">g</span> <span class="k">in</span> <span class="p">[</span><span class="s2">&#34;hello&#34;, &#34;choom&#34;, &#34;wake up samurai&#34;</span><span class="p">]</span> <span class="err">:</span> <span class="k">title</span><span class="p">(</span><span class="k">g</span><span class="p">)])</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p><code>random(list)</code> returns a uniformly random element of a list as a string. Did anyone ask for this? Absolutely not, but you deserve nice things.</p>
<h2 id="lol-lmfao">
  <a class="heading-link" href="#lol-lmfao">lol&hellip; lmfao.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The July post ended with me musing about adding <code>*.tfvars</code>-style functionality and asking, and I quote, &ldquo;Surely, I won&rsquo;t fall for this again?&rdquo;</p>
<figure class="pull-quote">
  <span class="pull-quote-mark" aria-hidden="true"><span class="icon "><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg></span></span>
  <blockquote>
    <p>He, indeed, fell for this again.</p>
  </blockquote>
  <figcaption>
    <cite>narrator</cite>
  </figcaption>
</figure>

<p>Yeah, so <code>v0.1.4</code> also ships with <code>--var-file</code> support:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="c1"># gig.vars
</span></span></span><span class="line"><span class="cl"><span class="n">district</span> <span class="o">=</span> <span class="s2">&#34;japantown&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">fixer</span>    <span class="o">=</span> <span class="s2">&#34;wakako&#34;</span></span></span></code></pre></div>
</div>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var-file gig.vars
</span></span><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var-file gig.vars --var <span class="nv">fixer</span><span class="o">=</span>dino
</span></span></code></pre></div>
</div>
<p>A var file is plain HCL; a <code>name = value</code> attributes written once per line. I originally supported JSON files too and then ripped that out before release, because maintaining a second parse path for a format nobody was going to use failed the smell test. In other words, &ldquo;YAGNI&rdquo;.</p>
<p>Precedence works the way you&rsquo;d expect: defaults go first, a var file overrides defaults and an explicit <code>--var</code> flag always has the final say. In the second command above <code>fixer</code> comes out as <code>dino</code> no matter what the file thinks.</p>
<p>Strictness from the flag side applies here too. An entry in a var file that names a variable the profile never declared is an error. Values are coerced to their declared types and every problem in the file is reported all at once for convenience.</p>
<h2 id="our-first-contributor-has-entered-the-chat">
  <a class="heading-link" href="#our-first-contributor-has-entered-the-chat">Our first contributor has entered the chat!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>This release contains the first fix from an actual outside contributor. <a href="https://github.com/ThreeToes">ThreeToes</a> tracked down an annoying initialisation bug where the first window in a profile would be ignored and cleaned up on startup. Leaving you with either a stray default terminal or only the subsequent windows.</p>
<p>Someone who isn&rsquo;t me not only ran the thing, they read the source and fixed it. That&rsquo;s a genuinely lovely milestone for a hobby project. Thanks, man!</p>
<p>Oh, and the repo finally has a proper <a href="https://github.com/wilhelm-murdoch/glazier/blob/main/SPEC.md">SPEC.md</a>. A full reference for the profile format describing every block, namespace and function.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Release notes are like backups. Everyone agrees they&rsquo;re important and nobody does them until it&rsquo;s embarrassing.</p>
<p>As always, it&rsquo;s <a href="https://github.com/wilhelm-murdoch/glazier">up on GitHub</a> under MIT. If you&rsquo;ve got Go installed:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> go install github.com/wilhelm-murdoch/glazier/cmd/glaze@latest
</span></span></code></pre></div>
</div>
<p>Or, grab a prebuilt binary from the <a href="https://github.com/wilhelm-murdoch/glazier/releases">releases page</a>, checksums and provenance attestations are included. Small note on this, I do plan on adding support for all your favourite package managers to make this process a bit more intuitive. Homebrew first as a test run, then all the other you&rsquo;d expect.</p>
<p>If you&rsquo;ve opinions on <code>locals</code>, var files or what Glazier should learn next, the comments are right below.</p>
<p>P.S. - I&rsquo;ve another secret project in the works and I&rsquo;m using it to declaratively generate Open Graph social cards for the blog. I thought I&rsquo;d create a format dedicated to Glazier releases.</p>
<figure class="img-single">
    <button type="button" class="zoom" aria-label="Zoom image: The Open Graph release card for Glazier v0.5.1">
      <img
  src="https://wilhelm.codes/blog/glazier-release-015/og_hu_3d6b8ec9a330023c.webp"
  srcset="/blog/glazier-release-015/og_hu_3d6b8ec9a330023c.webp 736w, /blog/glazier-release-015/og_hu_d7182b2892d9721b.webp 1104w, /blog/glazier-release-015/og_hu_e05a468bc0a12ed2.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="386"
  alt="The Open Graph release card for Glazier v0.5.1"
  loading="lazy"
  decoding="async"
  data-full="/blog/glazier-release-015/og_hu_ca00927e46de581.webp"
  data-credit="Generated using HCL and Ogre..."
  data-credit-href="https://wilhelm.codes"
/>

    </button>
    
      <figcaption>
        
          <a href="https://wilhelm.codes" target="_blank" rel="noopener">Generated using HCL and Ogre...</a>
        
      </figcaption>
    
  </figure>]]></content:encoded></item><item><title>Errors are Values</title><link>https://wilhelm.codes/bits/link-errors-are-values/</link><pubDate>Sun, 16 Aug 2026 10:00:00 +1000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/bits/link-errors-are-values/</guid><category>go</category><wc:kind>bit</wc:kind><wc:bit>link</wc:bit><wc:source>The Go Blog</wc:source><wc:target>https://go.dev/blog/errors-are-values</wc:target><description>It&amp;rsquo;s an oldie, but I always go back to it. The post I keep sending to people who tell me Go error handling is just noise. The point was never that you type if err != nil a lot. The point is that an error is an ordinary value, so you can do ordinary things with it.</description><content:encoded><![CDATA[<p>It&rsquo;s an oldie, but I always go back to it. The post I keep sending to people who tell me Go error handling is just noise. The point was never that you type <code>if err != nil</code> a lot. The point is that an error is an ordinary value, so you can do ordinary things with it.</p>
]]></content:encoded></item><item><title>“A little copying is better than a little dependency.”</title><link>https://wilhelm.codes/bits/quote-rob-pike/</link><pubDate>Sun, 26 Jul 2026 07:45:00 +1000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/bits/quote-rob-pike/</guid><category>go</category><category>wisdom</category><wc:kind>bit</wc:kind><wc:bit>quote</wc:bit><wc:untitled>true</wc:untitled><wc:source>Rob Pike, Go Proverbs</wc:source><description>&amp;ldquo;A little copying is better than a little dependency.&amp;rdquo;</description><content:encoded><![CDATA[<p>&ldquo;A little copying is better than a little dependency.&rdquo;</p>
]]></content:encoded></item><item><title>It's OK to Let Go</title><link>https://wilhelm.codes/blog/its-ok-to-let-go/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/its-ok-to-let-go/</guid><category>go</category><category>open-source</category><wc:kind>post</wc:kind><description>So, I&amp;rsquo;m a sucker for wordplay and puns. Anyway&amp;hellip;
There&amp;rsquo;s a line in my shell history that goes back to 2022: go get github.com/wilhelm-murdoch/go-collection. For years it was the first dependency into almost every Go project I tinkered with, sometimes before I&amp;rsquo;d even give the idea a solid shape. I&amp;rsquo;d open Neovim, initialise a module and then my little collection library got pulled in. Just like muscle memory.
Today I&amp;rsquo;m archiving it. Not because it broke and not because I got bored of it, but because the Go standard library quietly walked up and did its job better than it ever could. This post is part explanation and part eulogy, because I think we&amp;rsquo;re generally quite bad at ending software on purpose and it&amp;rsquo;s worth practising in public.</description><content:encoded><![CDATA[<p>So, I&rsquo;m a sucker for wordplay and puns. Anyway&hellip;</p>
<p>There&rsquo;s a line in my shell history that goes back to 2022: <code>go get github.com/wilhelm-murdoch/go-collection</code>. For years it was the first dependency into almost every Go project I tinkered with, sometimes before I&rsquo;d even give the idea a solid shape. I&rsquo;d open Neovim, initialise a module and then my little collection library got pulled in. Just like muscle memory.</p>
<p>Today I&rsquo;m archiving it. Not because it broke and not because I got bored of it, but because the Go standard library quietly walked up and did its job better than it ever could. This post is part explanation and part eulogy, because I think we&rsquo;re generally quite bad at ending software on purpose and it&rsquo;s worth practising in public.</p>
<h2 id="why-even-write-it-in-the-first-place">
  <a class="heading-link" href="#why-even-write-it-in-the-first-place">Why even write it in the first place?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Waaaay back in 2022 Go 1.18 had just shipped generics after roughly a decade of the community asking, arguing and writing increasingly unhinged <code>interface{}</code> workarounds. I wanted to actually <em>learn</em> the new type parameter machinery rather than just read about it and I was also deeply tired of writing the same <code>for</code> loop to check whether a slice contained a thing for the hundredth time.</p>
<p>So <a href="https://github.com/wilhelm-murdoch/go-collection">go-collection</a> happened. A single generic <code>Collection[T]</code> type wrapping a slice, with all the conveniences I kept reaching for: <code>Contains</code>, <code>Find</code>, <code>Filter</code>, <code>Map</code>, <code>Sort</code>, <code>Batch</code>, <code>Push</code>, <code>Pop</code> and a few dozen friends. All chainable, fluent, tested and small.</p>
<figure class="pull-quote">
  <span class="pull-quote-mark" aria-hidden="true"><span class="icon "><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg></span></span>
  <blockquote>
    <p>There are far more comprehensive modules out there, but this one works quite well for my purposes.</p>
  </blockquote>
  <figcaption>
    <cite>me, in the README, setting expectations appropriately for once</cite>
  </figcaption>
</figure>

<p>That sentence was doing more work than I realised at the time. It wasn&rsquo;t trying to compete with the big functional-utility libraries. It was scratching a personal itch and for a few years it scratched it well. Then Go&rsquo;s standard library grew and slurped most of it up.</p>
<h2 id="taking-a-trip-to-the-morgue">
  <a class="heading-link" href="#taking-a-trip-to-the-morgue">Taking a trip to the morgue.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Cause of death is easy to establish here, because it happened in three well-documented releases. Go 1.21 shipped the <code>slices</code> package. Go 1.22 and 1.23 finished the job. Let&rsquo;s walk through it, method by method, like the nerds we are.</p>
<h3 id="does-it-have-the-thing">
  <a class="heading-link" href="#does-it-have-the-thing">Does it have the thing?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>The bread and butter. Probably eighty percent of my actual usage of this library was some flavour of &ldquo;is this thing in there?&rdquo;:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">fruits</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">collection</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;apple&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;orange&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;strawberry&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fruits</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="s">&#34;orange&#34;</span><span class="p">)</span><span class="w"> </span><span class="c1">// true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fruits</span><span class="p">.</span><span class="nf">FindIndex</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">i</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">item</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;str&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w"> </span><span class="c1">// 2</span></span></span></code></pre></div>
</div>
<p>As of Go 1.21, the standard library does both on a plain slice with no wrapper type in sight:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">fruits</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;apple&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;orange&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;strawberry&#34;</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">slices</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">fruits</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;orange&#34;</span><span class="p">)</span><span class="w"> </span><span class="c1">// true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">slices</span><span class="p">.</span><span class="nf">IndexFunc</span><span class="p">(</span><span class="nx">fruits</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">item</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;str&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w"> </span><span class="c1">// 2</span></span></span></code></pre></div>
</div>
<p>No <code>New</code>, no <code>.Items()</code> to unwrap at the end when some other API wants a real slice. It&rsquo;s just my function with better ergonomics and a compiler team maintaining it. I&rsquo;m fine with this.</p>
<h3 id="sorting-where-i-dont-even-get-to-feel-bitter">
  <a class="heading-link" href="#sorting-where-i-dont-even-get-to-feel-bitter">Sorting, where I don&rsquo;t even get to feel bitter.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Here&rsquo;s my <code>Sort</code>. I want you to look at what using it actually required:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">numbers</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">collection</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">numbers</span><span class="p">.</span><span class="nf">Sort</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">j</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">left</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">numbers</span><span class="p">.</span><span class="nf">At</span><span class="p">(</span><span class="nx">i</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">right</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">numbers</span><span class="p">.</span><span class="nf">At</span><span class="p">(</span><span class="nx">j</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">left</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">right</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div>
</div>
<p>Index-based comparators, so you had to reach <em>back into the collection you were currently sorting</em> to get at the values. I inherited that design from the old <code>sort.Slice</code> idiom and it was clunky then too. The modern equivalent:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">numbers</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">slices</span><span class="p">.</span><span class="nf">Sort</span><span class="p">(</span><span class="nx">numbers</span><span class="p">)</span></span></span></code></pre></div>
</div>
<p>And when you&rsquo;re sorting something with actual structure, <code>SortFunc</code> hands you the two elements directly, with <code>cmp.Compare</code> doing the boring part:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">slices</span><span class="p">.</span><span class="nf">SortFunc</span><span class="p">(</span><span class="nx">people</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="nx">Person</span><span class="p">)</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">cmp</span><span class="p">.</span><span class="nf">Compare</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">Age</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nx">Age</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div>
</div>
<p>This isn&rsquo;t a case of the standard library catching up to my library. It blew my version right out of the water.</p>
<h3 id="the-killing-blow-came-with-iterators">
  <a class="heading-link" href="#the-killing-blow-came-with-iterators">The killing blow came with iterators.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Go 1.22 added <code>slices.Concat</code>, which retired my <code>Concat</code>. Fine, that one was three lines anyway. But Go 1.23 shipped range-over-function iterators and with them <code>slices.Chunk</code>, and that&rsquo;s the release where I stopped pretending this was still a contest. <code>Batch</code> was one of the few methods I was genuinely proud of:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">records</span><span class="p">.</span><span class="nf">Batch</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">batch</span><span class="p">,</span><span class="w"> </span><span class="nx">index</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">item</span><span class="w"> </span><span class="nx">Record</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// process item as part of batch N</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">},</span><span class="w"> </span><span class="mi">100</span><span class="p">)</span></span></span></code></pre></div>
</div>
<p>Now it&rsquo;s a language feature wearing a standard library hat:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">chunk</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">slices</span><span class="p">.</span><span class="nf">Chunk</span><span class="p">(</span><span class="nx">records</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// chunk is a []Record of up to 100 items</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>A real <code>for</code> loop. You can <code>break</code> out of it, <code>continue</code> past things and return early. My callback-based version couldn&rsquo;t do any of that without contorting itself. The trouble is that a wrapper library can only ever be as expressive as the language it wraps and the language just grew.</p>
<h3 id="the-stuff-that-never-needed-a-library">
  <a class="heading-link" href="#the-stuff-that-never-needed-a-library">The stuff that never needed a library.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>An honest confession while we&rsquo;re standing over the body: a decent chunk of the API never deserved to exist in the first place. <code>Push</code>, <code>Pop</code>, <code>Shift</code>, <code>Unshift</code>. I wrote JavaScript for years before Go was ever a public thing and apparently nobody walks away from that unscathed.</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">c</span><span class="p">.</span><span class="nf">Push</span><span class="p">(</span><span class="s">&#34;e&#34;</span><span class="p">)</span><span class="w">         </span><span class="c1">// s = append(s, &#34;e&#34;)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">item</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nf">Pop</span><span class="p">()</span><span class="w"> </span><span class="c1">// item = s[len(s)-1]; s = s[:len(s)-1]</span></span></span></code></pre></div>
</div>
<p>The comments on the right are the entire implementation. Idiomatic Go slice handling was already good at this in 2012. Those methods existed because my fingers missed <code>Array.prototype</code>, not because Go was missing anything.</p>
<h3 id="the-survivors-if-you-could-even-call-them-that">
  <a class="heading-link" href="#the-survivors-if-you-could-even-call-them-that">The &ldquo;survivors&rdquo;, if you could even call them that.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Here&rsquo;s the wrinkle that makes this more interesting than a straight obituary. <code>Map</code>, <code>Filter</code>, <code>Reduce</code>, <code>Some</code>, <code>None</code>; the functional stalwarts <em>still</em> aren&rsquo;t in the standard library, all these years later. On paper, that&rsquo;s the corner of the market my little module could have retired to. A niche! Sustained relevance!</p>
<p>Except, no. First, because my <code>Map</code> had a secret shame:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">c</span><span class="w"> </span><span class="o">*</span><span class="nx">Collection</span><span class="p">[</span><span class="nx">T</span><span class="p">])</span><span class="w"> </span><span class="nf">Map</span><span class="p">(</span><span class="nx">f</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">out</span><span class="w"> </span><span class="nx">Collection</span><span class="p">[</span><span class="nx">T</span><span class="p">])</span></span></span></code></pre></div>
</div>
<p>See it? <code>T</code> in, <code>T</code> out. Go methods can&rsquo;t introduce new type parameters, so a method-based <code>Map</code> can never change the element type. Strings to strings, ints to ints. A <code>Map</code> that can only map onto itself is a <code>Map</code> in vibes only; the moment you want lengths of strings you&rsquo;re back to writing a loop anyway.</p>
<p>And second, more fundamentally: the Go community collectively shrugged and decided the loop was fine. The proposal discussions happened, the experiments ran and the ecosystem largely settled on this:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">names</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">ducks</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">duck</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">ducks</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">names</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">ToUpper</span><span class="p">(</span><span class="nx">duck</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>Four lines. Obvious to anyone who&rsquo;s read any Go at all, no import, nothing to learn, nothing to maintain and debugger-friendly. Even where <code>go-collection</code> wasn&rsquo;t superseded, it turned out to be unnecessary, which is somehow a more thorough defeat.</p>
<h2 id="software-is-a-tool-not-a-legacy">
  <a class="heading-link" href="#software-is-a-tool-not-a-legacy">Software is a tool, not a legacy.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>So that&rsquo;s the technical half. Here&rsquo;s the part I actually wanted to write about.</p>
<p>We don&rsquo;t really have a culture of <em>finishing</em> software. A project is either &ldquo;actively maintained&rdquo; or it&rsquo;s &ldquo;abandoned&rdquo;, with all the guilt that word drags behind it. Issues accumulate, badges rot and the README slowly becomes a lie because nobody wants to be the person who admits the thing is done, or no longer necessary. I&rsquo;ve had this module sitting in that limbo for a while now, still getting pulled into new projects out of habit while a strictly better replacement sat in the standard library the whole time.</p>
<p>But software isn&rsquo;t a legacy; it&rsquo;s a tool. Tools serve a purpose. This one&rsquo;s purpose was to teach me generics in their first year of existence and to spare me a few thousand <code>for</code> loops while the language sorted itself out. Purpose served on both counts. And of all the ways for a library to die, being absorbed by the standard library is the best one available. It means the gap you filled was real and now it&rsquo;s just <em>gone</em>, fixed at the correct layer, for everyone and forever. That&rsquo;s not a failure state. That&rsquo;s the mission succeeding so hard the mission ceases to exist.</p>
<p>Keeping it alive from here would mean maintaining a slightly worse, slightly slower, personally-branded copy of <code>slices</code> out of pure sentiment. Nobody needs that, least of all me. I&rsquo;m busy enough and would much rather move on to newer, more interesting things.</p>
<h2 id="the-practical-bits">
  <a class="heading-link" href="#the-practical-bits">The practical bits.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>So today, the repo gets the full send-off:</p>
<ul>
<li>A deprecation notice at the top of the README, pointing anyone who lands there at the <a href="https://pkg.go.dev/slices">slices</a> package and at this post.</li>
<li>The repository gets <a href="https://github.com/wilhelm-murdoch/go-collection">archived</a> on GitHub. Read-only, clearly signposted and locked in eternal stasis.</li>
</ul>
<p>If you&rsquo;re one of the however-many people with this module in a <code>go.mod</code> somewhere nothing breaks. The Go module proxy caches published versions more or less permanently, so <code>v1.0.11</code> will keep resolving long after we&rsquo;re all dust. Archiving just makes the end of our relationship official rather than leaving it implied.</p>
<p>If you&rsquo;ve got a little module of your own in the same situation that has been superseded, purpose spent, kept alive out of habit, then consider this your permission slip. Write the notice, hit archive and go build that next thing. An honest ending is a kindness to your users and to yourself. It&rsquo;s OK to let go.</p>
<p><a href="https://github.com/wilhelm-murdoch/glazier/commit/8b6b84a220225fe714b8623aac1b183d64faf09a">Here</a> is an example of me tearing <code>go-collection</code> out of my very recent Glazier project. This one stung a little at how easily it was replaced.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Thanks, little buddy. You were there before the ecosystem knew what generics were supposed to look like, you appeared in more of my <code>go.mod</code> files than any dependency I didn&rsquo;t write, and you were, right to the end, exactly as comprehensive as I needed you to be.</p>
<p><code>go get</code> in peace.</p>
]]></content:encoded></item><item><title>I Added Variable Block Support To Glazier</title><link>https://wilhelm.codes/blog/i-couldnt-talk-myself-out-of-typed-variables/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/i-couldnt-talk-myself-out-of-typed-variables/</guid><category>go</category><category>hcl</category><category>tmux</category><category>glazier</category><wc:kind>post</wc:kind><description>I knew the moment I thought about potentially adding variable definition blocks as a first-class feature Glazier it was going to bother me until I went about implementing it. So, as expected, a week later, here we are. I am happy to report that I&amp;rsquo;ve maintained my spotless record of not talking myself out of something tricky.
So, over the course of the past few days Glazier learned a new trick. Profiles can now declare the variables they accept, give them primitive types, mark them as required and read them back through a proper var. namespace.</description><content:encoded><![CDATA[<p>I knew the moment I thought about potentially adding variable definition blocks as a first-class feature Glazier it was going to bother me until I went about implementing it. So, as expected, a week later, here we are. I am happy to report that I&rsquo;ve maintained my spotless record of <em>not</em> talking myself out of something tricky.</p>
<p>So, over the course of the past few days Glazier learned a new trick. Profiles can now declare the variables they accept, give them primitive types, mark them as required and read them back through a proper <code>var.</code> namespace.</p>
<h2 id="out-with-the-old">
  <a class="heading-link" href="#out-with-the-old">Out with the old.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Technically speaking, Glazier has had variable support since day one. You could pass <code>--var region=watson</code> and reference <code>${region}</code> anywhere in your profile and that was that. Anything you passed got dumped into a flat namespace and any name you <em>referenced</em>, but forgot to set, quietly resolved to an empty string.</p>
<p>That&rsquo;s fine right up until you fat-finger <code>${reigon}</code> and spend ten minutes wondering why your session name has a hole in it. The whole reason I built this thing was to get Terraform&rsquo;s &ldquo;here is exactly what you did wrong and where&rdquo; experience.</p>
<p>Letting variables fail silently just felt a bit like an own-goal.</p>
<h2 id="profiles-can-now-declare-what-they-accept">
  <a class="heading-link" href="#profiles-can-now-declare-what-they-accept">Profiles can now declare what they accept.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Because this whole thing is kind of a love letter to Terraform&rsquo;s parser, the idea behind variable definitions is functionally the same. You declare a <code>variable</code> block, it sits at the top level next to your session and that&rsquo;s the input you&rsquo;re allowed to pass:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;district&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  description</span> <span class="o">=</span> <span class="s2">&#34;the district the gig is themed after&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  type</span>        <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl"><span class="n">  default</span>     <span class="o">=</span> <span class="s2">&#34;watson&#34;</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;fixer&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  type</span> <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;gig-${var.district}&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;${var.fixer}-ops&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;echo ${var.fixer} has the next job&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>First, you reach a variable through <code>var.</code> and <em>only</em> <code>var.</code> now, so it reads exactly like it would in a <code>.tf</code> file. Second, <code>district</code> has a default and <code>fixer</code> doesn&rsquo;t, which turns out to matter quite a lot in a second.</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var <span class="nv">fixer</span><span class="o">=</span>wakako <span class="c1"># district falls back to &#34;watson&#34;</span>
</span></span><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var <span class="nv">district</span><span class="o">=</span>arasaka --var <span class="nv">fixer</span><span class="o">=</span>wakako
</span></span></code></pre></div>
</div>
<p>So, here you are using the same templated profile to define two seperate sessions decided entirely by flags.</p>
<h2 id="pass-it-something-it-doesnt-know-and-it-says-so">
  <a class="heading-link" href="#pass-it-something-it-doesnt-know-and-it-says-so">Pass it something it doesn&rsquo;t know and it says so.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>If you pass a <code>--var</code> for a variable the profile never declared, you no longer get a silent shrug. You get a proper error:</p>
<div class="code-block">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Undefined variable
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">A value for &#34;ghost&#34; was passed with --var, but the profile declares no
</span></span><span class="line"><span class="cl">variable &#34;ghost&#34;. Add a variable &#34;ghost&#34; {} block, or remove the flag.</span></span></code></pre></div>
</div>
<p>I went back and forth on this one. Being strict about undeclared flags is the sort of thing that feels pedantic until the day it catches a typo you&rsquo;d have otherwise chased for half an hour. In this case strict won.</p>
<p>A <code>--var</code> you can&rsquo;t explain is almost always a mistake, so Glazier now treats it like one.</p>
<h2 id="no-default-means-you-have-to-mean-it">
  <a class="heading-link" href="#no-default-means-you-have-to-mean-it">No default means you have to mean it.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A variable <em>without</em> a default is required. Leave it unset and <code>up</code> refuses to do anything until you supply it:</p>
<div class="code-block">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Missing required variable
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  on .glaze line 6, in variable &#34;fixer&#34;:
</span></span><span class="line"><span class="cl">   6: variable &#34;fixer&#34; {
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The variable &#34;fixer&#34; has no default, so a value must be supplied with
</span></span><span class="line"><span class="cl">--var fixer=&lt;value&gt;.</span></span></code></pre></div>
</div>
<p>This is the bit that makes a profile a genuine contract rather than a suggestion. If a layout is useless without a fixer, declare <code>fixer</code> with no default and the tool will make absolutely sure you provided one before it touches tmux.</p>
<h2 id="proper-type-checking">
  <a class="heading-link" href="#proper-type-checking">Proper type-checking.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Every variable declares a <code>type</code>. Use one of the bare keywords <code>string</code>, <code>number</code> or <code>bool</code>. The value you hand over on the command line is coerced into that type and rejected if it doesn&rsquo;t fit:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;base_index&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  type</span>    <span class="o">=</span> <span class="k">number</span>
</span></span><span class="line"><span class="cl"><span class="n">  default</span> <span class="o">=</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<div class="code-block">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ glaze up --var base_index=two
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Error: Invalid variable value
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  on .glaze line 1, in variable &#34;base_index&#34;:
</span></span><span class="line"><span class="cl">   1: variable &#34;base_index&#34; {
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The value passed for variable &#34;base_index&#34; with --var cannot be used as
</span></span><span class="line"><span class="cl">number: a number is required.</span></span></code></pre></div>
</div>
<p><code>two</code> is not a number, so you find out <em>before</em> anything launches rather than after tmux has already swallowed a garbage option and started behaving oddly. Booleans get the same treatment: <code>true</code> and <code>false</code> are fine, anything else is shown the door. Unfortunately, I decided not to go with truthy or falsey value coercion.</p>
<p>I kept it to the three primitives on purpose. The moment you add <code>list(string)</code> and friends you&rsquo;re also signing up for coercing comma-soup off the command line into nested types. That&rsquo;s a much bigger can of worms than a tmux helper needs to open today.</p>
<p>Though, obviously, if you disagree you&rsquo;re more than welcome to leave a comment below!</p>
<h2 id="a-namespace-for-everything">
  <a class="heading-link" href="#a-namespace-for-everything">A namespace for everything.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I&rsquo;ll be honest about the consequence here. This is a breaking change and pretending otherwise would be rude. The old flat <code>${region}</code> style is gone. If a value comes from a declared variable, you read it as <code>var.region</code>.</p>
<p>Which raised a genuinely interesting design question I sat with for a while. If variables are only ever used <em>inside</em> a session, why not declare them inside the session block too? It feels more intuitive at first though, but it&rsquo;s also wrong.</p>
<p>However you nest the declaration, the <em>reference</em> is still flat and global; you write <code>var.fixer</code>, never <code>session.var.fixer</code>. Declaring something file-global but reading it block-scoped is incoherent and a variable is really an input to the <em>invocation</em>, sitting conceptually above the session and not a property of it. So they live at the top level, exactly where Terraform puts them. I borrowed both the syntax and the reasoning.</p>
<h2 id="where-did-the-environment-variables-go">
  <a class="heading-link" href="#where-did-the-environment-variables-go">Where did the environment variables go?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>They also got their own namespace. Previously <code>GLAZE_ENV_token</code> showed up as a bare <code>${token}</code>, sharing the same flat namespace as everything else. Now it lives under an <code>env.</code> namespace, which makes the three sources nicely symmetric:</p>
<ul>
<li><code>var.*</code> for variables you declared and passed with <code>--var</code></li>
<li><code>env.*</code> for <code>GLAZE_ENV_*</code> environment variables</li>
<li><code>path.pwd</code> &amp; <code>path.base</code> the unchanged built-ins</li>
</ul>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;host-${env.box}&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {}
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> <span class="nv">GLAZE_ENV_box</span><span class="o">=</span>nightcity glaze up
</span></span></code></pre></div>
</div>
<div class="code-block">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">session = host-nightcity</span></span></code></pre></div>
</div>
<p>There&rsquo;s no declaration needed for <code>env.*</code>; the environment is the environment. Only the things you pass with <code>--var</code> have to be declared, because those are the inputs <em>you</em> are claiming the profile accepts.</p>
<h2 id="tearing-down-a-session">
  <a class="heading-link" href="#tearing-down-a-session">Tearing down a session.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>If a required variable now blocks <code>up</code>, doesn&rsquo;t that wreck <code>glaze down</code>? I recently taught the <code>down</code> command to evaluate <em>only</em> the session name so it doesn&rsquo;t need every variable buried deep in the profile just to kill a session. Surely required variables undo that?</p>
<p>They don&rsquo;t and this was the fiddly bit to get right. <code>down</code> resolves variables leniently. A variable used solely in some pane command, far away from the session name, is neither required nor evaluated when you&rsquo;re tearing things down:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;beep&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  type</span> <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;daemon-run&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;echo ${var.beep}&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze down
</span></span><span class="line"><span class="cl"><span class="go">INF nothing to do; session is not running session=daemon-run
</span></span></span></code></pre></div>
</div>
<p>If <code>beep</code> isn&rsquo;t provided, the command just accepts it and moves on, because <code>down</code> never needed it. <code>up</code> enforces the full contract; <code>down</code> only asks for what the name actually depends on. Obviously, the exception here is if a variable is used to construct the actual session name. Other than that, this is an idempotent no-op.</p>
<h2 id="validation-has-been-updated-too">
  <a class="heading-link" href="#validation-has-been-updated-too">Validation has been updated too.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p><code>glaze format --validate</code> decodes a profile and reports diagnostics without ever touching tmux and it now enforces the <em>entire</em> variable contract. Miss a required variable and validation fails right there:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze format --validate
</span></span><span class="line"><span class="cl"><span class="go">Error: Missing required variable
</span></span></span><span class="line"><span class="cl"><span class="go">  ...
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="gp">$</span> glaze format --validate --var <span class="nv">region</span><span class="o">=</span>us-east-1
</span></span><span class="line"><span class="cl"><span class="gp">#</span> clean. nothing to report.
</span></span></code></pre></div>
</div>
<p>So, you can lint a parameterised profile in CI, or just before committing it and know it holds together before you ever try to bring it up.</p>
<h2 id="this-literally-broke-all-my-current-profiles">
  <a class="heading-link" href="#this-literally-broke-all-my-current-profiles">This literally broke all my current profiles.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>And I regret nothing. The flat-namespace migration touched pretty much everything; sample profiles, the docs, a pile of tests and all the profiles I actually use day to day. Normally this is the part of a post where I&rsquo;d sketch out a careful migration path and apologise profusely.</p>
<p>But, the entire userbase of this tool is almost, as far as I can tell, me. So the migration path is &ldquo;I&rsquo;ll fix all my profiles tomorrow morning over coffee.&rdquo;</p>
<figure class="pull-quote">
  <span class="pull-quote-mark" aria-hidden="true"><span class="icon "><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg></span></span>
  <blockquote>
    <p>There is a real freedom in shipping a breaking change to an audience you can fit in a mirror.</p>
  </blockquote>
  <figcaption>
    <cite>it me.</cite>
  </figcaption>
</figure>

<p>To migrate is easy enough, just define your variables and prefix all references with <code>var.</code>. This <em>is</em> still currently in beta after all.</p>
<h2 id="in-closing">
  <a class="heading-link" href="#in-closing">In closing&hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Under the hood this turned into a more interesting refactor than I expected. <code>hcldec</code> wants to decode a whole file body against one spec and it flatly refuses to tolerate a <code>variable</code> block sitting next to the session it doesn&rsquo;t know about. The trick was to stop handing it the whole file, pull the session block out myself and decode <em>just its body</em>, which leaves the variable blocks to be gathered and resolved in their own quiet little pass beforehand. There&rsquo;s a tidy <code>PartialContent</code> implementation in there that was heaps of fun to figure out.</p>
<p>None of which a tmux wrapper strictly needed, of course. But &ldquo;strictly needed&rdquo; stopped being the bar around the time I added a fuzzer. The point was to feel out how Terraform turns a typo into a friendly, located error and now Glazier does a respectable impression of it for variables too.</p>
<p>I&rsquo;m still ironing out all the kinks and ensuring I still have an 80% baseline in testing and code coverage, but I&rsquo;m planning to release this and a few bug fixes and dependency bumps this weekend. Tomorrow-me gets to read this back with fresh eyes and decide how much of it past-me oversold. Very nice.</p>
<p>Until I merge it in, you can play with the latest release <a href="https://github.com/wilhelm-murdoch/glazier">here</a>.</p>
<p>And now I&rsquo;m thinking about adding <code>*.tfvars</code>-like functionality. Surely, I won&rsquo;t fall for this again?</p>
]]></content:encoded></item><item><title>Why I Built Glazier</title><link>https://wilhelm.codes/blog/why-i-built-glazier/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/why-i-built-glazier/</guid><category>go</category><category>tmux</category><category>hcl</category><category>glazier</category><wc:kind>post</wc:kind><description>I live in tmux. I typically have an editor here, a dev server there, logs tailing in the corner or a spare pane for poking at things. The trouble is that this little world is frustratingly ephemeral. Rebooting my machine ( thank you, compuslory MacOS updates ), kill the wrong session or just close the laptop lid for too long and it can all evaporate. Then, I&amp;rsquo;m back to rebuilding the same layout by hand, one split-window at a time, like some kind of animal.</description><content:encoded><![CDATA[<p>I live in <a href="https://github.com/tmux/tmux/wiki">tmux</a>. I typically have an editor here, a dev server there, logs tailing in the corner or a spare pane for poking at things. The trouble is that this little world is frustratingly ephemeral. Rebooting my machine ( thank you, compuslory MacOS updates ), kill the wrong session or just close the laptop lid for too long and it can all evaporate. Then, I&rsquo;m back to rebuilding the same layout by hand, one <code>split-window</code> at a time, like some kind of animal.</p>
<p>So I built <a href="https://github.com/wilhelm-murdoch/glazier">Glazier</a>; a small command-line tool that lets me describe a tmux workspace once and recreate it on demand. Type <code>glaze up</code> and the sessions, windows and panes I described spring back into existence exactly how I left them.</p>
<p>This has been a slow-burning labour of love for the better part of two years and it&rsquo;s finally in a state where I feel comfortable letting other people look at it. So let&rsquo;s talk about why it exists, what else is out there and how this one is different.</p>
<h2 id="its-just-a-config-file-right">
  <a class="heading-link" href="#its-just-a-config-file-right">It&rsquo;s just a config file, right?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>That was the idea, at least. I just wanted to stop rebuilding the same layouts over and over. But, as is tradition, I didn&rsquo;t want to make it <em>too</em> easy for myself.</p>
<p>There was a second, more selfish motivation. As a platform engineer, there isn&rsquo;t a day that goes by where I don&rsquo;t work with <a href="https://www.terraform.io/">Terraform</a>. I&rsquo;ve always been quietly fascinated by how it parses and validates its configuration. That whole experience of getting a precise, friendly error pointing at the exact line you fat-fingered, rather than a stack trace and a 🖕. I wanted to understand how that machinery actually worked.</p>
<p>So, I&rsquo;m a heavy tmux user <em>and</em> I wanted to learn HCL parsing from the inside. These two things lined up a little too perfectly. So Glazier&rsquo;s profiles aren&rsquo;t YAML; they&rsquo;re <a href="https://github.com/hashicorp/hcl">HCL</a>. The same configuration language Terraform uses and parsed with the same underlying library.</p>
<p>Here&rsquo;s a basic profile:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;daemon-run&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span>   <span class="o">=</span> <span class="s2">&#34;ice-breaker&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    layout</span> <span class="o">=</span> <span class="s2">&#34;main-vertical&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;nvim ./payloads&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;watch -n1 netwatch --target arasaka-mainframe&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Drop that in a file called <code>.glaze</code>, run <code>glaze up</code> next to it and you&rsquo;re jacked in.</p>
<h2 id="im-not-the-first-to-do-this-not-even-the-3rd-or-the-4th">
  <a class="heading-link" href="#im-not-the-first-to-do-this-not-even-the-3rd-or-the-4th">I&rsquo;m not the first to do this; not even the 3rd&hellip; or the 4th.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>This is a thoroughly-solved problem and I&rsquo;d be doing you a disservice if I pretended otherwise. There&rsquo;s a whole shelf of mature, battle-tested tools that do effectively the same thing:</p>
<ul>
<li><a href="https://github.com/tmuxinator/tmuxinator">tmuxinator</a> is the one most are familiar with. Written in Ruby with YAML profiles. Probably what most people reach for.</li>
<li><a href="https://github.com/remi/teamocil">teamocil</a> is also written Ruby; also uses YAML.</li>
<li><a href="https://github.com/ivaaaan/smug">smug</a> is written in Go and uses YAML and is the closest in spirit to Glazier if we&rsquo;re being honest.</li>
<li><a href="https://github.com/tmux-python/tmuxp">tmuxp</a> is written in good&rsquo;ole reliable Python and it&rsquo;ll happily eat YAML <em>or</em> JSON.</li>
</ul>
<p>If you already use and trust one of these, I&rsquo;ll be straight with you: there isn&rsquo;t a compelling reason to switch. Keep using what works. I&rsquo;m not here to convince anyone to rip out a tool they&rsquo;re happy with.</p>
<p>But if you&rsquo;re still reading, here&rsquo;s what <em>I</em> like about mine.</p>
<h2 id="so-whats-actually-different">
  <a class="heading-link" href="#so-whats-actually-different">So what&rsquo;s actually different?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<h3 id="the-profile-validates-itself">
  <a class="heading-link" href="#the-profile-validates-itself">The profile validates itself!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>This is the part I set out to build, so it&rsquo;s the part I&rsquo;m fondest of. Because Glazier is built on HCL, it inherits Terraform-style diagnostics for free. Mistype a layout, point a starting directory at somewhere that doesn&rsquo;t exist or forget a required block and you don&rsquo;t get a vague &ldquo;something went wrong fuck you&rdquo;. You get told exactly what and where you messed up:</p>
<div class="code-block">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Invalid layout specified
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  on .glaze line 4, in session.window:
</span></span><span class="line"><span class="cl">   4:     layout = &#34;main-plumbus&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The layout value of &#34;main-plumbus&#34; is not a supported preset
</span></span><span class="line"><span class="cl">(even-horizontal, even-vertical, main-horizontal, main-vertical,
</span></span><span class="line"><span class="cl">tiled) nor a valid tmux layout string.</span></span></code></pre></div>
</div>
<p>There&rsquo;s a <code>glaze format</code> command that rewrites your profile into a canonical style and, with <code>--validate</code>, reports any of these diagnostics without touching tmux at all. Both of these scratch exactly the itch that started the whole project.</p>
<h3 id="variables-templates-and-string-functions-oh-my">
  <a class="heading-link" href="#variables-templates-and-string-functions-oh-my">Variables, templates and string functions! Oh, my!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>A static layout is useful. A <em>templated</em> one is better. Profiles can reference variables and you can feed those in from <code>--var</code> flags or <code>GLAZE_ENV_*</code> environment variables:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span>               <span class="o">=</span> <span class="s2">&#34;ops-${region}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  starting_directory</span> <span class="o">=</span> <span class="k">path</span><span class="p">.</span><span class="k">pwd</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="k">upper</span><span class="p">(</span><span class="k">region</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;k9s --context ${region}&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var <span class="nv">region</span><span class="o">=</span>ap-southeast-2
</span></span></code></pre></div>
</div>
<p>Look familiar? If you work with Terraform it does!</p>
<p>There&rsquo;s a handful of built-in string functions too. <code>upper</code>, <code>lower</code>, <code>replace</code>, <code>trimspace</code>, <code>join</code> and friends which act as thin wrappers over the same <code>go-cty</code> standard library Terraform uses. Plus, a couple of freebies like <code>path.pwd</code> and <code>path.base</code> so a profile can adapt to wherever it&rsquo;s run from.</p>
<h3 id="it-doesnt-lie-to-you-about-timing">
  <a class="heading-link" href="#it-doesnt-lie-to-you-about-timing">It doesn&rsquo;t lie to you about timing.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>This is the bit of engineering I&rsquo;m quietly proudest of, even though nobody will ever see it. When you fire a sequence of commands into a tmux pane, the naive approach is to blast them in with a <code>sleep</code> between each one and hope the previous command finished. That&rsquo;s flaky and it&rsquo;s how a few other tools handle it.</p>
<p>Glazier instead serialises commands through tmux&rsquo;s own <a href="https://man.openbsd.org/tmux#wait-for"><code>wait-for</code></a> signalling, so each command genuinely waits for the previous one to finish before the next is sent. There are no fixed sleeps and no races. The <em>one</em> exception is the final command in a list, which is sent fire-and-forget. If your last command is a long-running dev server or <code>tail -f</code>, waiting on it would hang <code>up</code> forever.</p>
<p>I learned that last part the hard way.</p>
<h3 id="it-can-mostly-save-a-session">
  <a class="heading-link" href="#it-can-mostly-save-a-session">It can <em>mostly</em> save a session!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Run <code>glaze save</code> inside a live session and it&rsquo;ll capture the structure back out into a profile: windows, panes, names, layouts, focus and starting directories. The &ldquo;mostly&rdquo; is doing some load-bearing work in that sentence and it&rsquo;s a deliberate choice, not a missing feature.</p>
<p><code>save</code> will <strong>not</strong> export your pane commands, environment variables, hooks or tmux options. Why? Because each of those is a footgun:</p>
<ul>
<li><strong>Commands</strong> would re-execute on the next <code>up</code>. A forgotten <code>rm -rf</code> captured from some pane could ruin your whole day on replay.</li>
<li><strong>Environment variables</strong> can only be read as the <em>entire</em> session environment. This means inherited secrets, tokens and keys getting written into a file you might commit. That&rsquo;s a big fat no from me, dawg.</li>
<li><strong>Options</strong> read back as effective state, hopelessly tangling up with your <code>tmux.conf</code> and manual tweaks.</li>
</ul>
<p>So, a saved profile is a scaffold. It gets the geometry right and you add back the commands and config you actually want by hand.</p>
<h2 id="a-real-profile">
  <a class="heading-link" href="#a-real-profile">A real profile.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Enough talk. Here&rsquo;s the profile I actually use when working this website:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HCL</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="k">path</span><span class="p">.</span><span class="k">base</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">  envs</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">    HUGO_GITHUB_TOKEN</span>        <span class="o">=</span> <span class="s2">&#34;${github}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    HUGO_ACTIVITY_GRAPH_DEMO</span> <span class="o">=</span> <span class="s2">&#34;${demo}&#34;</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span>  <span class="o">=</span> <span class="s2">&#34;editor&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;nvim&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span>  <span class="o">=</span> <span class="s2">&#34;terminal&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    focus</span> <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {}
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;server&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;hugo server --disableFastRender&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;git&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;lazygit&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Running <code>glaze up --var github=*** --var demo=true</code> gives me the following windows:</p>
<ul>
<li>My editor of choice; NeoVim.</li>
<li>A dedicated terminal session.</li>
<li>The Hugo server along with some <code>HUGO_</code> specific environment variables; values sourced from the CLI.</li>
<li>Finally, <code>lazygit</code> to manage and commit my changes.</li>
</ul>
<p>Further, the <code>--var</code> arguments aren&rsquo;t really necessary as you could just <code>export GLAZE_var_name</code> as well and Glazier will pick it up automatically.</p>
<p>Anyways, because the session is just a named thing, the rest of the lifecycle is tidy too. List what&rsquo;s running with <code>glaze ls</code>:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze ls
</span></span><span class="line"><span class="cl"><span class="go">NAME      WINDOWS  PATH
</span></span></span><span class="line"><span class="cl"><span class="go">glazier*  2        /home/wilhelm/Development/wilhelm.codes
</span></span></span><span class="line"><span class="cl"><span class="go">scratch   1        /tmp
</span></span></span></code></pre></div>
</div>
<p>The asterisk marks the session I&rsquo;m currently attached to. When I&rsquo;m done, I tear it down by profile:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze down
</span></span></code></pre></div>
</div>
<p><code>down</code> is idempotent and that&rsquo;s on purpose. Bringing down a session that isn&rsquo;t running is a no-op, not an error, so it&rsquo;s safe to drop in scripts without defensive checks.</p>
<h2 id="some-over-engineered-bits-">
  <a class="heading-link" href="#some-over-engineered-bits-">Some over-engineered bits &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>To be frank a tool that shells out to tmux did not strictly <em>need</em> a fuzzed HCL parser, build provenance attestations on its release binaries or a CI pipeline that runs the test suite against multiple Go versions and operating systems.</p>
<p>But, that was never really the point. The point was learning how Terraform&rsquo;s parser ticks, working out how to drive tmux reliably without sleeps and over-engineering the ever-loving-shit out of an already-solved problem because it was <em>fun</em>. Every constraint I imposed upon myself taught me something new. This is ultimately the only metric I actually care about for a hobby project like this.</p>
<h2 id="where-to-get-it">
  <a class="heading-link" href="#where-to-get-it">Where to get it?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>It&rsquo;s <a href="https://github.com/wilhelm-murdoch/glazier">up on GitHub</a> with a shiny MIT license. If you&rsquo;ve got Go installed:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Console</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> go install github.com/wilhelm-murdoch/glazier/cmd/glaze@latest
</span></span></code></pre></div>
</div>
<p>Or, grab a prebuilt binary from the <a href="https://github.com/wilhelm-murdoch/glazier/releases">releases page</a>. I currently build for Linux and macOS on both <code>amd64</code> and <code>arm64</code>. Each one ships with a checksum and a signed provenance attestation, because of course it does.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I would confidently say Glazier has moved on from &ldquo;experimental&rdquo; to &ldquo;stable&rdquo;. It works and I use it every single day. But, there are rough edges and there may be any number of unencountered failure modes. The <code>down</code> and <code>ls</code> commands only landed recently and I&rsquo;m not sure if the latter should remain. I&rsquo;ve also got a running list of ideas I haven&rsquo;t talked myself out of yet. Like, something similar to Terraform&rsquo;s <code>*.tfvars</code> files or defining typed variables.</p>
<p>If you&rsquo;re already happy with Tmuxinator or Smug, then stick with them. But if the idea of a declarative, self-validating, slightly-too-clever tmux profile appeals to you, or you just want to read some Go that wraps tmux in ways it was probably never meant to be wrapped, I&rsquo;d love for you to take it for a spin.</p>
<p>I sincerely hope you find <a href="https://github.com/wilhelm-murdoch/glazier">Glazier</a> as useful as I had fun building it.</p>]]></content:encoded></item></channel></rss>