<?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 · Hcl</title><link>https://wilhelm.codes/tags/hcl/</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/hcl/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>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>Welp. I’ve convinced myself that Glazier needs Terraform-style …</title><link>https://wilhelm.codes/bits/blurb-ive-convinced-myself-glazier-needs-variable-blocks/</link><pubDate>Wed, 24 Jun 2026 10:19:56 +1000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/bits/blurb-ive-convinced-myself-glazier-needs-variable-blocks/</guid><category>glazier</category><category>hcl</category><wc:kind>bit</wc:kind><wc:bit>blurb</wc:bit><wc:untitled>true</wc:untitled><description>Welp. I&amp;rsquo;ve convinced myself that Glazier needs Terraform-style variable declaration blocks to validate --var flags. I&amp;rsquo;m doing it and I will not be stopped. I&amp;rsquo;m thinking something as simple as the following session block-adjecent top-level spec:
HCL variable &amp;#34;name&amp;#34; { description = optional(string) type = required(string | bool | number) default = optional(type) } Time to do what I do best: make things more complicated than they have any right to be.</description><content:encoded><![CDATA[<p>Welp. I&rsquo;ve convinced myself that Glazier needs Terraform-style variable declaration blocks to validate <code>--var</code> flags. I&rsquo;m doing it and I will not be stopped. I&rsquo;m thinking something as simple as the following <code>session</code> block-adjecent top-level spec:</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;name&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  description</span> <span class="o">=</span> <span class="k">optional</span><span class="p">(</span><span class="k">string</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">  type</span>        <span class="o">=</span> <span class="k">required</span><span class="p">(</span><span class="k">string</span> <span class="err">|</span> <span class="k">bool</span> <span class="err">|</span> <span class="k">number</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">  default</span>     <span class="o">=</span> <span class="k">optional</span><span class="p">(</span><span class="k">type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Time to do what I do best: make things more complicated than they have any right to be.</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>