<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.wiserfirst.com/feed.xml" rel="self" type="application/atom+xml"/><link href="https://www.wiserfirst.com/" rel="alternate" type="text/html"/><updated>2026-07-20T00:03:04+10:00</updated><id>https://www.wiserfirst.com/feed.xml</id><title type="html">Peaceful Revolution</title><subtitle>Just another personal blog</subtitle><author><name>Qing Wu</name></author><entry><title type="html">How to Use asdf Version Manager on macOS (2026 Update)</title><link href="https://www.wiserfirst.com/blog/how-to-use-asdf-on-macos-2026-update/" rel="alternate" type="text/html" title="How to Use asdf Version Manager on macOS (2026 Update)"/><published>2026-07-19T23:30:00+10:00</published><updated>2026-07-19T23:30:00+10:00</updated><id>https://www.wiserfirst.com/blog/how-to-use-asdf-on-macos-2026-update</id><content type="html" xml:base="https://www.wiserfirst.com/blog/how-to-use-asdf-on-macos-2026-update/"><![CDATA[<p>Five years ago, I wrote <a href="/blog/how-to-use-asdf-on-macos/">How to Use asdf Version Manager on macOS</a>, and it went on to become one of the most popular posts on my tiny blog site. There is just one problem: as years went by, many things changed in asdf, especially after the full rewrite in Go. If you follow my old guide today, almost nothing works. The rewrite removed most of the commands that guide was built around. <code class="language-plaintext highlighter-rouge">asdf global</code> is gone. <code class="language-plaintext highlighter-rouge">asdf local</code> is gone. Even the way to install it has changed.</p> <p>So here is the 2026 updated guide for asdf: same tool, same philosophy, new binary, new commands. Whether you are upgrading or starting fresh, this will get you productive with asdf.</p> <h2 id="why-asdf">Why asdf</h2> <p>Say you work as a developer for a company whose stack is Ruby on Rails on the backend and React on the frontend. The backend is not one repository but several, and they do not all run the same Ruby version; the frontend repositories are in the same boat with Node.js.</p> <p>To manage the different versions of Ruby you could use <a href="https://github.com/rbenv/rbenv">rbenv</a>, for Node.js there is <a href="https://github.com/nvm-sh/nvm">nvm</a>, and when Python enters the picture for some machine learning tasks, along comes <a href="https://github.com/pyenv/pyenv">pyenv</a>. Three tools with three slightly different command syntaxes is already a burden, and it only gets worse when you want to build a side project with Elixir or learn some Rust.</p> <p>asdf replaces all of them. Thanks to its plugin system, one tool with one set of commands can install and manage versions of almost any programming language, along with plenty of other CLI tools. And if a language or tool does not have a plugin yet, you can <a href="https://asdf-vm.com/plugins/create.html">create one</a> yourself.</p> <h2 id="what-changed-in-the-go-rewrite">What Changed in the Go Rewrite</h2> <p>In early 2025, asdf was rewritten from the ground up in Go and shipped as version 0.16.0. Until then, asdf was implemented in shell scripts, and the <code class="language-plaintext highlighter-rouge">asdf</code> command you ran was actually a shell function sourced into your session. It is now a single compiled binary.</p> <p>I will admit the rewrite did not win me over right away. The changes felt dramatic: suddenly everything was new in a tool I knew inside out. The way the upgrade was pushed did not help either. After 0.16.0 came out, my still-working installation started printing a long red warning on every single asdf command, telling me to migrate. Since every tool installed through asdf runs through asdf, that wall of red text greeted every <code class="language-plaintext highlighter-rouge">ruby</code> and <code class="language-plaintext highlighter-rouge">node</code> invocation too, even though, by the notice’s own admission, the old version “works as it did in asdf version 0.15.0 and older”. Nothing was broken; I was just being nagged.</p> <p>Having lived with the new asdf for a while, though, I have come around. Everything feels snappier, though speed was never my complaint after years of daily use. What won me over were the quieter wins from the switch to a real binary: you now install and upgrade asdf through your package manager like any other tool, and there is nothing to source into your shell config.</p> <p>A rewrite of the internals alone would not warrant a new guide. The breaking changes do:</p> <ul> <li><code class="language-plaintext highlighter-rouge">asdf global</code> and <code class="language-plaintext highlighter-rouge">asdf local</code> are gone, replaced by a single <code class="language-plaintext highlighter-rouge">asdf set</code> command with flags for the different scopes.</li> <li><code class="language-plaintext highlighter-rouge">asdf shell</code> is gone. A binary cannot modify the environment of your running shell, so per-session overrides are now done with plain environment variables.</li> <li>Hyphenated commands lost their hyphens: <code class="language-plaintext highlighter-rouge">asdf plugin-add</code> became <code class="language-plaintext highlighter-rouge">asdf plugin add</code>, and <code class="language-plaintext highlighter-rouge">asdf list-all</code> became <code class="language-plaintext highlighter-rouge">asdf list all</code>.</li> <li><code class="language-plaintext highlighter-rouge">asdf update</code> is gone. You upgrade asdf itself through your package manager instead.</li> <li>The shell setup changed. Instead of sourcing <code class="language-plaintext highlighter-rouge">asdf.sh</code> in your shell config, you put the shims directory on your <code class="language-plaintext highlighter-rouge">PATH</code>.</li> </ul> <p>Do not worry if some of these terms mean nothing to you yet: they are all covered below. The latest release as of this writing is <code class="language-plaintext highlighter-rouge">v0.20.0</code>, which is the version this guide uses.</p> <h2 id="install-asdf">Install asdf</h2> <p>The simplest way to install asdf on macOS is with Homebrew:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>asdf
</code></pre></div></div> <p>Then add the asdf shims directory to your <code class="language-plaintext highlighter-rouge">PATH</code> (shims are explained in <a href="#under-the-hood">Under the Hood</a> below). For Zsh, add the following to the bottom of <code class="language-plaintext highlighter-rouge">~/.zshrc</code>:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">ASDF_DATA_DIR</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/.asdf</span><span class="k">}</span><span class="s2">/shims:</span><span class="nv">$PATH</span><span class="s2">"</span>
</code></pre></div></div> <p>Open a new terminal tab and you should be ready to use asdf :tada:</p> <p>In the previous version of this guide, I recommended installing asdf by cloning its Git repository and sourcing <code class="language-plaintext highlighter-rouge">asdf.sh</code> from your shell config. Neither applies anymore: asdf is a compiled binary now, so there is nothing to source. If you are upgrading from a pre-0.16 installation, remove the old <code class="language-plaintext highlighter-rouge">. $HOME/.asdf/asdf.sh</code> line from your <code class="language-plaintext highlighter-rouge">~/.zshrc</code> when you add the <code class="language-plaintext highlighter-rouge">PATH</code> line above.</p> <p>Homebrew is not the only option. You can also download a prebuilt binary from the <a href="https://github.com/asdf-vm/asdf/releases">asdf releases</a> page and put it somewhere on your <code class="language-plaintext highlighter-rouge">PATH</code>, or install it with <code class="language-plaintext highlighter-rouge">go install</code> if you already have Go set up. If you use Bash, Fish or another shell, refer to the <a href="https://asdf-vm.com/guide/getting-started.html">Getting Started</a> guide for the matching setup instructions.</p> <h2 id="manage-plugins">Manage Plugins</h2> <p>Before you can install Ruby, Node.js or anything else, you need to add the appropriate plugins. Plugins are how asdf knows how to handle each language or tool:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf plugin add ruby
asdf plugin add nodejs
</code></pre></div></div> <p>For plugins listed in the <a href="https://github.com/asdf-vm/asdf-plugins">asdf plugins repository</a>, the short name above is all you need. A plugin that is not listed there can still be added by passing its Git URL, shown here with the Node.js plugin standing in as an example:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
</code></pre></div></div> <p>In practice you will rarely need this: the plugins repository covers hundreds of tools.</p> <p>You can list the plugins you have installed:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf plugin list
</code></pre></div></div> <p>Or list all the plugins available in the plugins repository:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf plugin list all
</code></pre></div></div> <p>Plugins are also where new version definitions come from. If a freshly released version does not show up in <code class="language-plaintext highlighter-rouge">asdf list all</code>, update your plugins first:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf plugin update <span class="nt">--all</span>
</code></pre></div></div> <h2 id="manage-language-versions">Manage Language Versions</h2> <h3 id="install-versions">Install Versions</h3> <p>Suppose we want the current stable Ruby and the current LTS release of Node.js, which are <code class="language-plaintext highlighter-rouge">4.0.6</code> and <code class="language-plaintext highlighter-rouge">24.18.0</code> respectively as of this writing:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf <span class="nb">install </span>ruby 4.0.6
asdf <span class="nb">install </span>nodejs 24.18.0
</code></pre></div></div> <p>You can browse the available versions first:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf list all ruby
</code></pre></div></div> <p>Or skip looking up version numbers entirely:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf <span class="nb">install </span>nodejs latest
</code></pre></div></div> <p>To see which versions you already have installed, and to remove one you no longer need:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf list ruby
asdf uninstall ruby 3.3.6
</code></pre></div></div> <blockquote> <p>If a version fails to install, check the GitHub repository of the plugin: known issues and their fixes are usually documented there.</p> </blockquote> <h3 id="set-versions">Set Versions</h3> <p>This is the biggest change from the old command set. Where you previously chose between <code class="language-plaintext highlighter-rouge">asdf global</code> and <code class="language-plaintext highlighter-rouge">asdf local</code>, there is now a single command:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf <span class="nb">set </span>nodejs 24.18.0
</code></pre></div></div> <p>By default, <code class="language-plaintext highlighter-rouge">asdf set</code> writes to a <code class="language-plaintext highlighter-rouge">.tool-versions</code> file in the current directory, which is exactly what <code class="language-plaintext highlighter-rouge">asdf local</code> used to do. Run it in a project directory and that project is pinned to Node.js <code class="language-plaintext highlighter-rouge">24.18.0</code>, regardless of what other projects use.</p> <p>To set a user-wide default version, the equivalent of the old <code class="language-plaintext highlighter-rouge">asdf global</code>, pass the <code class="language-plaintext highlighter-rouge">-u</code> flag (for the home directory):</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf <span class="nb">set</span> <span class="nt">-u</span> nodejs 24.18.0
asdf <span class="nb">set</span> <span class="nt">-u</span> ruby 4.0.6
</code></pre></div></div> <p>The asdf team dropped the “global” terminology on purpose: it was never truly global, just a fallback in your home directory that any project could override. The new naming makes that explicit, and to me this is one place where the rewrite improved not just the implementation but the logic.</p> <p>There is also a <code class="language-plaintext highlighter-rouge">-p</code> flag, which updates the nearest existing <code class="language-plaintext highlighter-rouge">.tool-versions</code> file in a parent directory. This is handy in a monorepo, where you might run the command deep inside a subdirectory but want to update the version pinned at the repository root.</p> <p>Finally, if you want a tool to fall back to whatever macOS or Homebrew provides, you can set its version to <code class="language-plaintext highlighter-rouge">system</code>:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf <span class="nb">set </span>ruby system
</code></pre></div></div> <h3 id="session-overrides">Session Overrides</h3> <p>The old guide had a section on <code class="language-plaintext highlighter-rouge">asdf shell</code>, which set a version for just the current shell session. That command is gone, but the underlying mechanism survives: asdf still honours an environment variable named <code class="language-plaintext highlighter-rouge">ASDF_${TOOL}_VERSION</code>, and it takes precedence over any <code class="language-plaintext highlighter-rouge">.tool-versions</code> file.</p> <p>Say a legacy client app in your project only runs on an older Node.js. Instead of touching any files, you can override the version for one shell session:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">ASDF_NODEJS_VERSION</span><span class="o">=</span>20.19.0
</code></pre></div></div> <p>Or for a single command:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">ASDF_NODEJS_VERSION</span><span class="o">=</span>20.19.0 npm start
</code></pre></div></div> <p>Other sessions are unaffected, and closing the terminal cleans up after you.</p> <h3 id="quick-recap">Quick Recap</h3> <p>asdf lets you pin versions per directory through <code class="language-plaintext highlighter-rouge">.tool-versions</code> files, set user-wide defaults with <code class="language-plaintext highlighter-rouge">asdf set -u</code>, and override everything per session or per command with an environment variable. That should be flexible enough for most situations you will ever encounter.</p> <h2 id="under-the-hood">Under the Hood</h2> <p>If you are new to asdf, this might sound like magic, but how asdf works is actually quite straightforward, and the Go rewrite made it easier to explain.</p> <h3 id="shims">Shims</h3> <p>The <code class="language-plaintext highlighter-rouge">PATH</code> entry you added during installation points at a directory of shims. A shim is a tiny executable that sits in front of the real one: when you run <code class="language-plaintext highlighter-rouge">node</code>, you are actually running the <code class="language-plaintext highlighter-rouge">node</code> shim, which asks asdf to resolve the right Node.js version and then executes that version’s real <code class="language-plaintext highlighter-rouge">node</code> binary. This is why there is nothing to source in your shell config anymore, and why any program, not just your interactive shell, picks up the correct versions.</p> <p>One maintenance command worth knowing here is <code class="language-plaintext highlighter-rouge">asdf reshim</code>. Shims are generated when a version is installed, so when a package you add later ships its own executable, think <code class="language-plaintext highlighter-rouge">gem install rubocop</code> or a global npm package, the shell may not find the new command. Regenerate the shims for that plugin and you are back in business:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf reshim ruby
</code></pre></div></div> <h3 id="version-resolution">Version Resolution</h3> <p>When a shim resolves a version, asdf checks the following, in order:</p> <ol> <li>The <code class="language-plaintext highlighter-rouge">ASDF_${TOOL}_VERSION</code> environment variable.</li> <li>A <code class="language-plaintext highlighter-rouge">.tool-versions</code> file in the current directory, then the parent directory, then the parent’s parent, all the way up.</li> <li>The <code class="language-plaintext highlighter-rouge">.tool-versions</code> file in your home directory, which is where <code class="language-plaintext highlighter-rouge">asdf set -u</code> writes.</li> </ol> <p>If you enable the <code class="language-plaintext highlighter-rouge">legacy_version_file</code> option described in <a href="#coexist-with-legacy-tools">Coexist with Legacy Tools</a> below, legacy files such as <code class="language-plaintext highlighter-rouge">.ruby-version</code> and <code class="language-plaintext highlighter-rouge">.nvmrc</code> are also consulted during step 2, right after <code class="language-plaintext highlighter-rouge">.tool-versions</code>.</p> <p>The <code class="language-plaintext highlighter-rouge">.tool-versions</code> file itself is plain text, one tool per line:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> ~/.tool-versions
nodejs 24.18.0
ruby 4.0.6
</code></pre></div></div> <p>If you are working on a personal project or your team has adopted asdf, it is a very good idea to commit the project’s <code class="language-plaintext highlighter-rouge">.tool-versions</code> file to version control. If your team has not agreed on asdf yet, add it to <code class="language-plaintext highlighter-rouge">.gitignore</code> instead, and see <a href="#coexist-with-legacy-tools">Coexist with Legacy Tools</a> below.</p> <h3 id="current-versions">Current Versions</h3> <p>To see which versions are active in the current directory and where they come from, run <code class="language-plaintext highlighter-rouge">asdf current</code>:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf current
Name    Version    Source                                    Installed
nodejs  24.18.0    /Users/username/project/.tool-versions    <span class="nb">true
</span>ruby    4.0.6      /Users/username/.tool-versions            <span class="nb">true</span>
</code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">Source</code> column shows exactly which <code class="language-plaintext highlighter-rouge">.tool-versions</code> file each version was resolved from, which makes it easy to figure out where a surprising version is coming from.</p> <h2 id="use-asdf-in-a-team">Use asdf in a Team</h2> <p>Coming back to the scenario from the beginning of this post: suppose your team has adopted asdf (and if it has not yet, the next section is for you). Everyone still needs the correct versions installed when they start working on a project for the first time.</p> <p>You could read the project’s <code class="language-plaintext highlighter-rouge">.tool-versions</code> file and install each version by hand, but there is no need. Run <code class="language-plaintext highlighter-rouge">asdf install</code> in the project directory without any arguments:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf <span class="nb">install</span>
</code></pre></div></div> <p>asdf reads <code class="language-plaintext highlighter-rouge">.tool-versions</code> and installs anything that is missing. One command, and a new team member is ready to run the project. This remains one of my favourite asdf features, and it works exactly as it did before the rewrite.</p> <h2 id="coexist-with-legacy-tools">Coexist with Legacy Tools</h2> <p>If you are sold on asdf but your team still uses rbenv, nvm and friends, there is a configuration option that lets you meet them where they are. Create a <code class="language-plaintext highlighter-rouge">.asdfrc</code> file in your home directory with the following content:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>legacy_version_file <span class="o">=</span> <span class="nb">yes</span>
</code></pre></div></div> <p>With this set, asdf plugins will read the version files of other version managers, for example <code class="language-plaintext highlighter-rouge">.ruby-version</code> for rbenv and <code class="language-plaintext highlighter-rouge">.nvmrc</code> for nvm. Your teammates keep their existing tools and version files, and you get to use asdf against the same repositories.</p> <blockquote> <p>Note: not all plugins support this feature. If you rely on this behaviour, check the documentation of the plugins you use.</p> </blockquote> <h2 id="what-about-mise">What About mise?</h2> <p>A 2026 guide to asdf would not be complete without mentioning <a href="https://mise.jdx.dev/">mise</a>. mise started life as a Rust reimplementation of asdf, and has grown into a broader tool that also manages environment variables and project tasks. It understands <code class="language-plaintext highlighter-rouge">.tool-versions</code> files and can use asdf plugins, so the two coexist well, and the asdf documentation itself links to a <a href="https://mise.jdx.dev/dev-tools/comparison-to-asdf.html">comparison</a> between them.</p> <p>I have stuck with asdf: it does one thing well, and I have used it for years. Speed used to be the complaint you would hear most often, and mise is still faster on paper, but with the 2x to 7x speedups from the Go rewrite, there is no practical difference in daily use. If you want an all-in-one tool, though, mise is a good alternative.</p> <h2 id="summary">Summary</h2> <p>In this post, I covered:</p> <ul> <li>what changed in the asdf rewrite in Go</li> <li>how to install asdf</li> <li>how to add plugins</li> <li>how to manage versions with <code class="language-plaintext highlighter-rouge">asdf set</code></li> <li>how shims and version resolution work under the hood</li> <li>how asdf fits in a team setting</li> <li>how to use asdf alongside legacy tools</li> </ul> <p>The commands may have changed since 2021, but the reasons I recommended asdf back then have not: one tool, one set of commands, and a <code class="language-plaintext highlighter-rouge">.tool-versions</code> file that makes sharing language versions across a team effortless. If you run into anything this guide does not cover, the <a href="https://asdf-vm.com/">asdf documentation</a> remains your best first stop.</p>]]></content><author><name>Qing Wu</name></author><category term="asdf"/><category term="version-manager"/><category term="macos"/><category term="ruby"/><category term="nodejs"/><category term="golang"/><summary type="html"><![CDATA[A fresh guide to asdf after its ground-up rewrite in Go]]></summary></entry><entry><title type="html">Speeding Up Our Test Suite by 82%</title><link href="https://www.wiserfirst.com/blog/speeding-up-test-suite-by-82-percent/" rel="alternate" type="text/html" title="Speeding Up Our Test Suite by 82%"/><published>2026-07-11T00:00:00+10:00</published><updated>2026-07-11T00:00:00+10:00</updated><id>https://www.wiserfirst.com/blog/speeding-up-test-suite-by-82-percent</id><content type="html" xml:base="https://www.wiserfirst.com/blog/speeding-up-test-suite-by-82-percent/"><![CDATA[<h2 id="the-config-line-that-made-me-wince">The Config Line That Made Me Wince</h2> <p>Late last year I joined a new company. In my first week, poking around the Phoenix app I’d be working on, I found this in <code class="language-plaintext highlighter-rouge">config/test.exs</code>:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span> <span class="ss">:bcrypt_elixir</span><span class="p">,</span> <span class="ss">:log_rounds</span><span class="p">,</span> <span class="mi">1</span>
</code></pre></div></div> <p>Nobody on the team had written that line. The installer that scaffolded the app’s authentication generated it, as Phoenix’s own <code class="language-plaintext highlighter-rouge">phx.gen.auth</code> does for every new app. The <code class="language-plaintext highlighter-rouge">bcrypt_elixir</code> docs recommend the same thing plainly.</p> <p>I winced. Only months earlier, at my previous company, I had spent weeks “discovering” this exact fix: profiling, benchmarks, rounds of factory rewrites, using the best available model at the time.</p> <p>I even had a post drafted about that journey, documenting what felt like a solid performance win. It had been waiting on finishing touches I never got round to, and after that week they stopped mattering. So here it is instead, rewritten as a confession.</p> <h2 id="the-slow-suite">The Slow Suite</h2> <p>Rewind to my previous company. Our main Phoenix application had no reliable test suite. Some tests existed, but many failed for various reasons. That meant we couldn’t run them on CI, so they offered little assurance that new changes wouldn’t break existing functionality.</p> <p>We were a tiny team, just three developers, hammered by an endless stream of feature requests and bug reports. My colleagues stayed underwater in that stream. But I kept coming back to the Chinese saying 工欲善其事，必先利其器: to do good work, one must first sharpen one’s tools. So I took the test suite on.</p> <p>I fixed some failures, temporarily skipped others, and got the suite green. Then I set up a Jenkins pipeline so every change ran the tests. A real step forward, with a catch.</p> <p>The full suite took 285 seconds on my laptop, and even longer on CI. Nearly five minutes per run meant I avoided running it locally, and every build was a long wait.</p> <p>The slowness bothered me, but I had no clear picture of where the time was going and no obvious plan of attack. I couldn’t justify spending days on optimization without a concrete goal, so we lived with it for a long time.</p> <p>What eventually changed the calculus was AI-assisted coding. With models finally capable of serious work on a real codebase, an open-ended optimization effort stopped looking like a time sink I couldn’t afford.</p> <h2 id="finding-the-bottleneck">Finding the Bottleneck</h2> <p>Before optimizing anything, I profiled. The culprit revealed itself quickly: <strong>Bcrypt</strong>. Every call to <code class="language-plaintext highlighter-rouge">Bcrypt.hash_pwd_salt()</code> took 100–200ms.</p> <p>That might not sound like much, until you realize the suite called it hundreds of times. Every <code class="language-plaintext highlighter-rouge">insert(:user)</code>, <code class="language-plaintext highlighter-rouge">insert(:customer)</code>, and <code class="language-plaintext highlighter-rouge">insert(:provider)</code> in our ExMachina factories hashed a password:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">user_factory</span><span class="p">()</span> <span class="k">do</span>
  <span class="p">%</span><span class="no">User</span><span class="p">{</span>
    <span class="ss">username:</span> <span class="no">Internet</span><span class="o">.</span><span class="n">user_name</span><span class="p">(),</span>
    <span class="ss">email:</span> <span class="no">Internet</span><span class="o">.</span><span class="n">email</span><span class="p">(),</span>
    <span class="ss">password_hash:</span> <span class="no">Bcrypt</span><span class="o">.</span><span class="n">hash_pwd_salt</span><span class="p">(</span><span class="s2">"test_password_123"</span><span class="p">)</span>  <span class="c1"># 100-200ms each!</span>
  <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div> <p>Multiply that by hundreds of test setups, and the bottleneck was clear.</p> <h2 id="optimizing-the-hard-way">Optimizing the Hard Way</h2> <p>The work happened in Claude Code, on Opus 4, the strongest model of the day. My instruction was direct: benchmark the factories and optimize away the repeated hashing.</p> <p>And that is exactly what it did. Diligently. We pre-computed password hashes as module constants and reused them for all default test data, falling back to real hashing only for custom passwords:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Admin</span><span class="o">.</span><span class="no">UserFactory</span> <span class="k">do</span>
  <span class="c1"># Pre-computed hash for the default test password</span>
  <span class="nv">@user_test_password</span> <span class="s2">"test_password_123"</span>
  <span class="nv">@user_test_password_hash</span> <span class="s2">"$2b$12$MFkURc/nMXU5HQxgWURhauA4tegLJKvJPlOtvQxzvQ35oz0q/Nrj2"</span>

  <span class="k">def</span> <span class="n">user_factory</span><span class="p">(</span><span class="n">attrs</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">password</span> <span class="o">=</span> <span class="no">Map</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span> <span class="nv">@user_test_password</span><span class="p">)</span>

    <span class="c1"># Use pre-computed hash for default, generate for custom passwords</span>
    <span class="n">password_hash</span> <span class="o">=</span> <span class="no">Map</span><span class="o">.</span><span class="n">get_lazy</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span> <span class="ss">:password_hash</span><span class="p">,</span> <span class="k">fn</span> <span class="o">-&gt;</span>
      <span class="k">if</span> <span class="n">password</span> <span class="o">==</span> <span class="nv">@user_test_password</span> <span class="k">do</span>
        <span class="nv">@user_test_password_hash</span>  <span class="c1"># Fast: pre-computed</span>
      <span class="k">else</span>
        <span class="no">Bcrypt</span><span class="o">.</span><span class="n">hash_pwd_salt</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>  <span class="c1"># Slow: on-demand</span>
      <span class="k">end</span>
    <span class="k">end</span><span class="p">)</span>

    <span class="p">%</span><span class="no">User</span><span class="p">{</span>
      <span class="ss">username:</span> <span class="no">Internet</span><span class="o">.</span><span class="n">user_name</span><span class="p">(),</span>
      <span class="ss">email:</span> <span class="no">Internet</span><span class="o">.</span><span class="n">email</span><span class="p">(),</span>
      <span class="ss">password_hash:</span> <span class="n">password_hash</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div> <p>We applied this pattern across every authentication-related factory: users, customers, providers. Each got its own default test password and pre-computed hash.</p> <p>The results?</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Before</span>
<span class="nv">$ </span>mix <span class="nb">test</span>
....................................
Finished <span class="k">in </span>285.6 seconds

<span class="c"># After first optimization</span>
<span class="nv">$ </span>mix <span class="nb">test</span>
....................................
Finished <span class="k">in </span>105.2 seconds
</code></pre></div></div> <p><strong>~63% improvement.</strong> Nearly three minutes off the suite. Success!</p> <p>But notice what didn’t happen. The model knew the goal was a faster test suite, and, as I would soon find out, it knew the standard fix. Yet across all those rounds of benchmarking and refactoring, it never suggested <code class="language-plaintext highlighter-rouge">log_rounds</code>, or even checking the <code class="language-plaintext highlighter-rouge">bcrypt_elixir</code> docs. I had asked it to optimize factories, so it optimized factories.</p> <p>And something felt wrong to me. We had added real complexity (constants, conditionals, helper functions across multiple factory files) for a problem that every Elixir app hashing passwords must share. A problem this common shouldn’t need solving the hard way.</p> <h2 id="the-question-that-changed-everything">The Question That Changed Everything</h2> <p>If the problem was that common, someone must have solved it properly already. So I asked a different question: <strong>“Is there a cheaper alternative to <code class="language-plaintext highlighter-rouge">Bcrypt.hash_pwd_salt</code> for tests?”</strong></p> <p>The model came back with the fix almost immediately: <code class="language-plaintext highlighter-rouge">log_rounds</code>. It had known all along. It was just never asked.</p> <p>Bcrypt is intentionally slow. That’s the whole point: by making password hashing computationally expensive, it protects against brute-force attacks. The <code class="language-plaintext highlighter-rouge">log_rounds</code> parameter controls this cost:</p> <ul> <li><code class="language-plaintext highlighter-rouge">log_rounds: 12</code> means 2^12 = <strong>4,096 iterations</strong></li> <li><code class="language-plaintext highlighter-rouge">log_rounds: 10</code> means 2^10 = <strong>1,024 iterations</strong></li> <li><code class="language-plaintext highlighter-rouge">log_rounds: 4</code> means 2^4 = <strong>16 iterations</strong></li> </ul> <p>Each increment doubles the computational work. The default of 12 is right for production. But test passwords are ephemeral: they exist only during a test run and are immediately discarded. They only need to be valid hashes, not brute-force resistant.</p> <p>And crucially, <code class="language-plaintext highlighter-rouge">log_rounds</code> can be configured per environment:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/test.exs</span>
<span class="n">config</span> <span class="ss">:bcrypt_elixir</span><span class="p">,</span> <span class="ss">:log_rounds</span><span class="p">,</span> <span class="mi">4</span>
</code></pre></div></div> <p>One line. I reverted every factory change (the constants, the conditionals, the helpers) and the factories went back to their simple, original form:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">user_factory</span><span class="p">(</span><span class="n">attrs</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">password</span> <span class="o">=</span> <span class="no">Map</span><span class="o">.</span><span class="n">get_lazy</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span> <span class="k">fn</span> <span class="o">-&gt;</span> <span class="n">random_password</span><span class="p">()</span> <span class="k">end</span><span class="p">)</span>

  <span class="p">%</span><span class="no">User</span><span class="p">{</span>
    <span class="ss">username:</span> <span class="no">Internet</span><span class="o">.</span><span class="n">user_name</span><span class="p">(),</span>
    <span class="ss">email:</span> <span class="no">Internet</span><span class="o">.</span><span class="n">email</span><span class="p">(),</span>
    <span class="ss">password_hash:</span> <span class="no">Bcrypt</span><span class="o">.</span><span class="n">hash_pwd_salt</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>  <span class="c1"># Now fast!</span>
  <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div> <p>The results?</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># After second optimization</span>
<span class="nv">$ </span>mix <span class="nb">test</span>
....................................
Finished <span class="k">in </span>50.1 seconds
</code></pre></div></div> <p>From 285 seconds to 50: <strong>an 82% speedup</strong>, 5.7× faster, with less code than we started with. Production and dev configs stayed at 12, so there was no security compromise anywhere.</p> <h2 id="it-shouldnt-have-been-a-discovery">It Shouldn’t Have Been a Discovery</h2> <p>I drafted this story a couple of months after the fix. Knowing no better, I had framed it as a reasonable approach to a tough problem, with a big discovery at the end. It needed only some finishing touches, but life got busy, and the draft sat.</p> <p>Then came the new company, and that config line staring at me from a <code class="language-plaintext highlighter-rouge">test.exs</code> nobody had to write. I checked the <code class="language-plaintext highlighter-rouge">bcrypt_elixir</code> README. There it was: a low <code class="language-plaintext highlighter-rouge">log_rounds</code> setting for tests, documented clearly, all along.</p> <p>I was taken aback. My hard-won discovery was just boilerplate, knowledge so standard that app generators write it for you. For a good while I wasn’t sure this post deserved publishing at all.</p> <p>But that is exactly why it does. A tiny team lived with a five-minute test suite for years. The best model of the day ran rounds of benchmarks without once suggesting the docs. The fix was one documented line. The failure mode is the story.</p> <h2 id="what-i-actually-learned">What I Actually Learned</h2> <p><strong>The bottleneck was the question, not the intelligence.</strong> The model knew about <code class="language-plaintext highlighter-rouge">log_rounds</code> the entire time. Any of us could have found it in the README in five minutes. Nobody asked.</p> <p><strong>AI amplifies your framing; it doesn’t correct it.</strong> Asked to optimize factories, the model optimized factories: skillfully, and entirely within my flawed premise. Knowing my end goal didn’t help; my instructions had already chosen the approach. The quality of the answer is capped by the quality of the question.</p> <p><strong>Read the library docs before optimizing around the library.</strong> Bcrypt was never the problem. Using production settings in tests was, and the library authors had anticipated exactly that.</p> <p><strong>Listen to the “this is too hard” feeling.</strong> The pre-computed-hash detour wasn’t wasted. The discomfort of all that added complexity is what pushed me to stop and ask a better question.</p> <p><strong>Config can beat code.</strong> If your Elixir test suite is slow and uses Bcrypt, the fix is one line, and if your app’s authentication came from a generator like <code class="language-plaintext highlighter-rouge">phx.gen.auth</code>, it is probably already there:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/test.exs</span>
<span class="n">config</span> <span class="ss">:bcrypt_elixir</span><span class="p">,</span> <span class="ss">:log_rounds</span><span class="p">,</span> <span class="mi">1</span>
</code></pre></div></div> <p>Generators write 1; the <code class="language-plaintext highlighter-rouge">bcrypt_elixir</code> docs suggest 4, which is what we used. Either way, hashing becomes effectively free in tests.</p> <p>Sometimes the best optimization isn’t clever code, or even a smarter model. It’s noticing that a problem is too common to be this hard, then asking the question that assumes someone already solved it.</p>]]></content><author><name>Qing Wu</name></author><category term="elixir"/><category term="phoenix"/><category term="bcrypt"/><category term="testing"/><category term="ai"/><summary type="html"><![CDATA[A discovery that shouldn't have been necessary]]></summary></entry><entry><title type="html">Setting Up QMK on macOS the Modern Way</title><link href="https://www.wiserfirst.com/blog/setting-up-qmk-on-macos/" rel="alternate" type="text/html" title="Setting Up QMK on macOS the Modern Way"/><published>2026-06-25T10:00:00+10:00</published><updated>2026-06-25T10:00:00+10:00</updated><id>https://www.wiserfirst.com/blog/setting-up-qmk-on-macos</id><content type="html" xml:base="https://www.wiserfirst.com/blog/setting-up-qmk-on-macos/"><![CDATA[<p>If your keyboard supports QMK, you can rewrite its firmware yourself: remap any key, stack on layers, add macros and tap-hold behaviour, then flash the result straight onto the board. On macOS, the way you install the toolchain for this has quietly changed a while back. The old Homebrew recipe has been retired in favour of a self-contained installer that is far less likely to break on you. Here is how to set things up cleanly, and how to untangle an old Homebrew install if that is where you are starting from.</p> <p>Examples below use a ZSA Moonlander (<code class="language-plaintext highlighter-rouge">zsa/moonlander</code>) and a keymap called <code class="language-plaintext highlighter-rouge">mykeymap</code>. Swap in your own board and keymap name as you go.</p> <h2 id="quick-start">Quick start</h2> <ol> <li>Install the toolchain: <code class="language-plaintext highlighter-rouge">curl -fsSL https://install.qmk.fm | sh</code></li> <li>Fetch the QMK source: <code class="language-plaintext highlighter-rouge">qmk setup</code> (already have a clone? <code class="language-plaintext highlighter-rouge">qmk config user.qmk_home=/path/to/qmk_firmware</code> instead)</li> <li>Sanity-check: <code class="language-plaintext highlighter-rouge">qmk doctor</code> (optional if you ran <code class="language-plaintext highlighter-rouge">qmk setup</code>, which runs it for you)</li> <li>Keep your keymap in its own repo via External Userspace (details below)</li> <li>Build: <code class="language-plaintext highlighter-rouge">qmk compile -kb zsa/moonlander -km mykeymap</code></li> <li>Flash: <code class="language-plaintext highlighter-rouge">qmk flash -kb zsa/moonlander -km mykeymap</code>, then drop the board into its bootloader.</li> </ol> <h2 id="setting-up-qmk-recommended">Setting up QMK (recommended)</h2> <h3 id="what-youre-actually-installing">What you’re actually installing</h3> <p>The modern setup keeps everything tidy and self-contained, well away from your system Python and your day-to-day Homebrew packages. Three pieces get installed, all into user space:</p> <ul> <li>The <strong><code class="language-plaintext highlighter-rouge">qmk</code> CLI</strong>, running inside its own <code class="language-plaintext highlighter-rouge">uv</code>-managed Python environment, so a Python upgrade somewhere else on your machine cannot break it.</li> <li>The <strong>compilers</strong> (ARM, AVR, RISC-V), as a prebuilt bundle that QMK maintains and ships itself. You do not have to hunt down cross-compilers or babysit their versions.</li> <li>The <strong>flashing utilities</strong> (<code class="language-plaintext highlighter-rouge">dfu-util</code>, <code class="language-plaintext highlighter-rouge">avrdude</code>, and friends), bundled right alongside.</li> </ul> <p>When it is done, here is where things live by default, worth knowing so nothing feels like a black box:</p> <ul> <li><code class="language-plaintext highlighter-rouge">~/.local/bin/qmk</code>: the CLI, a symlink into <code class="language-plaintext highlighter-rouge">~/.local/share/uv/tools/qmk/</code></li> <li><code class="language-plaintext highlighter-rouge">~/.local/bin/uv</code>: the <code class="language-plaintext highlighter-rouge">uv</code> tool manager, if the script installed it</li> <li><code class="language-plaintext highlighter-rouge">~/Library/Application Support/qmk/</code>: the compilers and flashing tools</li> <li><code class="language-plaintext highlighter-rouge">~/Library/Application Support/qmk/qmk.ini</code>: your CLI config</li> </ul> <h3 id="1-install-the-cli-and-toolchains">1. Install the CLI and toolchains</h3> <p>One line does it:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://install.qmk.fm | sh
</code></pre></div></div> <p>Yes, it is a <code class="language-plaintext highlighter-rouge">curl … | sh</code>, but it is the genuine official installer: all it does is fetch QMK’s bootstrap script and run it. It gives you a 10-second countdown before touching anything (hit <code class="language-plaintext highlighter-rouge">Ctrl+C</code> if you change your mind), then installs the three pieces above.</p> <p>If you already have some of the prerequisites and do not want the installer fussing with them, you can opt out of individual steps. The handiest one on a machine already set up for development keeps it away from your Homebrew packages:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://install.qmk.fm | <span class="nv">SKIP_PACKAGE_MANAGER</span><span class="o">=</span>1 sh
</code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">SKIP_PACKAGE_MANAGER=1</code> stops the installer running <code class="language-plaintext highlighter-rouge">brew update</code>/<code class="language-plaintext highlighter-rouge">brew upgrade</code>, so it will not churn through your existing packages just to install a keyboard toolchain. (Already manage <code class="language-plaintext highlighter-rouge">uv</code> yourself? See <a href="#already-manage-uv-yourself">Already manage uv yourself?</a> below.)</p> <p>Curious what else you can toggle? Run the installer with <code class="language-plaintext highlighter-rouge">--help</code>:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://install.qmk.fm | sh <span class="nt">-s</span> <span class="nt">--</span> <span class="nt">--help</span>
</code></pre></div></div> <p>One last thing before you move on: make sure <code class="language-plaintext highlighter-rouge">~/.local/bin</code> is on your <code class="language-plaintext highlighter-rouge">PATH</code>, and ideally ahead of <code class="language-plaintext highlighter-rouge">/opt/homebrew/bin</code>, so the <code class="language-plaintext highlighter-rouge">qmk</code> you just installed is the one your shell actually reaches for. Drop this in your <code class="language-plaintext highlighter-rouge">~/.zshrc</code> if it is not already there:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.local/bin:</span><span class="nv">$PATH</span><span class="s2">"</span>
</code></pre></div></div> <p>Then open a fresh shell and confirm with <code class="language-plaintext highlighter-rouge">which qmk</code>. You want to see <code class="language-plaintext highlighter-rouge">/Users/you/.local/bin/qmk</code>.</p> <h3 id="2-tell-the-cli-where-the-qmk-source-lives">2. Tell the CLI where the QMK source lives</h3> <p>The CLI needs to know which <code class="language-plaintext highlighter-rouge">qmk_firmware</code> checkout to build from. That is the <code class="language-plaintext highlighter-rouge">qmk_home</code> setting.</p> <p>If you do not have the QMK source yet, let QMK grab it for you (pass your GitHub fork’s username if you have one):</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk setup
</code></pre></div></div> <p>That clones the repo into <code class="language-plaintext highlighter-rouge">~/qmk_firmware</code>, sets <code class="language-plaintext highlighter-rouge">qmk_home</code>, and runs <code class="language-plaintext highlighter-rouge">qmk doctor</code> to check things over.</p> <p>Already cloned it somewhere of your own? Then skip <code class="language-plaintext highlighter-rouge">qmk setup</code>. Running it would just clone a redundant second copy. Point the CLI at what you have instead:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk config user.qmk_home<span class="o">=</span>/Users/you/opensource/qmk_firmware
</code></pre></div></div> <p>A quick read-back to confirm it stuck:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk config user.qmk_home
</code></pre></div></div> <h3 id="3-check-everythings-happy">3. Check everything’s happy</h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk doctor
</code></pre></div></div> <p>Think of this as your pre-flight check. A clean run means the CLI, the compilers, and <code class="language-plaintext highlighter-rouge">qmk_home</code> are all talking to each other. If something is missing or misconfigured, this is where it will tell you, much nicer than finding out halfway through a build.</p> <h3 id="4-where-to-put-your-keymap">4. Where to put your keymap</h3> <p>The recommendation here is <strong>External Userspace</strong>: keep your keymap in its own repository, completely outside the QMK tree. It is the approach that ages best: your personal config stays in a clean repo you own, and the QMK source stays pristine, so pulling in upstream updates never tangles with your changes.</p> <p>The trick is that your external folder mirrors QMK’s own layout, so your keymap lives at:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;your-userspace&gt;/keyboards/zsa/moonlander/keymaps/mykeymap/
</code></pre></div></div> <p>You point QMK at the root of that userspace once:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk config user.overlay_dir<span class="o">=</span>/path/to/your/userspace
</code></pre></div></div> <p>From then on, the normal <code class="language-plaintext highlighter-rouge">qmk compile</code> command finds your keymap there automatically, with no difference in how you build. The full walkthrough, including how to scaffold the folder, lives in the <a href="https://docs.qmk.fm/newbs_external_userspace">QMK External Userspace docs</a>.</p> <p>If you would rather keep things dead simple and do not mind your keymap living inside the QMK checkout, the in-tree spot still works fine: drop the same <code class="language-plaintext highlighter-rouge">keymaps/mykeymap/</code> folder under <code class="language-plaintext highlighter-rouge">&lt;qmk_home&gt;/keyboards/zsa/moonlander/</code>. A lot of people version just that folder as its own git repo to get some of the separation benefits. It is a perfectly good starting point for a single keymap. The snag shows up once you have more than one: because each keymap lives in its own folder, each becomes its own separate git repo, so managing several, possibly across different keyboards, means a repo to track for every one. Your keymaps also show up as local changes inside the QMK tree.</p> <h3 id="5-build-it">5. Build it</h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk compile <span class="nt">-kb</span> zsa/moonlander <span class="nt">-km</span> mykeymap
</code></pre></div></div> <p>A green <code class="language-plaintext highlighter-rouge">[OK]</code> and a freshly generated <code class="language-plaintext highlighter-rouge">.bin</code> (or <code class="language-plaintext highlighter-rouge">.hex</code> on AVR boards) means you are good. You do not have to worry about the compiler being on your <code class="language-plaintext highlighter-rouge">PATH</code>. The CLI quietly wires up its bundled toolchain for you each time.</p> <h3 id="6-flash-it">6. Flash it</h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk flash <span class="nt">-kb</span> zsa/moonlander <span class="nt">-km</span> mykeymap
</code></pre></div></div> <p>This compiles and then sits waiting for your keyboard to show up in bootloader mode. To get a Moonlander there, do one of:</p> <ul> <li>Tap a <strong><code class="language-plaintext highlighter-rouge">QK_BOOT</code></strong> key, if your keymap has one mapped, or</li> <li>Press the physical <strong>reset button</strong> on top of the board.</li> </ul> <p>The moment it enters the bootloader, <code class="language-plaintext highlighter-rouge">qmk flash</code> spots it and writes the firmware. Prefer a GUI? ZSA’s <strong>Keymapp</strong> flashes the same firmware without the command line.</p> <h3 id="staying-up-to-date">Staying up to date</h3> <ul> <li><strong>The CLI</strong> is a <code class="language-plaintext highlighter-rouge">uv</code> tool, so <code class="language-plaintext highlighter-rouge">uv tool upgrade qmk</code> bumps it, or just re-run the install script.</li> <li><strong>The toolchains</strong> refresh by re-running the install script whenever you want the latest bundle.</li> <li><strong>The QMK source</strong> updates with a <code class="language-plaintext highlighter-rouge">git pull</code> in your <code class="language-plaintext highlighter-rouge">qmk_home</code>. Follow it with <code class="language-plaintext highlighter-rouge">git submodule update --init --recursive</code> to sync the submodules QMK pulls in. As long as you created your own keymap rather than updating the built-in keymaps in QMK, the pull stays conflict-free.</li> </ul> <h2 id="already-manage-uv-yourself">Already manage uv yourself?</h2> <p>If you already have <code class="language-plaintext highlighter-rouge">uv</code> installed, say via Homebrew or its standalone installer, there is no need to run the uv installer again. Pass <code class="language-plaintext highlighter-rouge">SKIP_UV=1</code> so the QMK installer reuses the <code class="language-plaintext highlighter-rouge">uv</code> you already have:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://install.qmk.fm | <span class="nv">SKIP_UV</span><span class="o">=</span>1 sh
</code></pre></div></div> <p>The only tradeoff is who owns updates: the <code class="language-plaintext highlighter-rouge">qmk</code> CLI still runs in its own uv-managed environment either way, but you keep <code class="language-plaintext highlighter-rouge">uv</code> itself current yourself (<code class="language-plaintext highlighter-rouge">brew upgrade uv</code>, or however you installed it) rather than re-running the QMK installer to refresh it.</p> <h2 id="if-you-set-up-qmk-the-old-way-via-homebrew">If you set up QMK the old way (via Homebrew)</h2> <p>Most likely you installed <code class="language-plaintext highlighter-rouge">qmk</code> and the ARM/AVR compilers from a handful of third-party Homebrew taps (<code class="language-plaintext highlighter-rouge">qmk/qmk</code>, <code class="language-plaintext highlighter-rouge">osx-cross/arm</code>, <code class="language-plaintext highlighter-rouge">osx-cross/avr</code>). For years that was just a few <code class="language-plaintext highlighter-rouge">brew tap</code> and <code class="language-plaintext highlighter-rouge">brew install</code> commands that ran without fuss. But that has recently changed: Homebrew 6.0.0 introduced Tap Trust. Because a tap is a Git repo of Ruby that runs with your privileges, it now refuses to load a non-official tap until you have explicitly trusted it. It is a healthy change, it surfaces a trust decision that used to be made silently on your behalf, but it also turns the old setup into a fiddlier and slightly nervier process of vouching for several third-party sources before anything will install. The self-contained installer above sidesteps all of that, which is why the tap-based recipe has been retired.</p> <h3 id="cleaning-out-the-old-homebrew-install">Cleaning out the old Homebrew install</h3> <p>Do this <strong>after</strong> the modern setup is working (<code class="language-plaintext highlighter-rouge">qmk doctor</code> green and a successful compile), so you are never without a working build. The order matters: uninstall the formulae first, then remove the taps, then sweep up orphans.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew uninstall <span class="nt">--force</span> qmk hid_bootloader_cli mdloader
brew uninstall arm-none-eabi-gcc@8 arm-none-eabi-binutils avr-gcc@8 avr-binutils
brew untap qmk/qmk osx-cross/arm osx-cross/avr
brew autoremove
</code></pre></div></div> <p>A few notes so nothing surprises you:</p> <ul> <li>The <code class="language-plaintext highlighter-rouge">--force</code> on the first line clears out all installed versions at once.</li> <li>You cannot untap a tap while its formulae are still installed, which is why the untap comes after the uninstalls.</li> <li><code class="language-plaintext highlighter-rouge">brew autoremove</code> mops up the long chain of dependencies the <code class="language-plaintext highlighter-rouge">qmk</code> formula originally dragged in (think Pillow → cairo/freetype/fonts, plus flashing tools like <code class="language-plaintext highlighter-rouge">avrdude</code>, <code class="language-plaintext highlighter-rouge">dfu-util</code>, <code class="language-plaintext highlighter-rouge">dfu-programmer</code>). It only removes things nothing else depends on, so shared tools like <code class="language-plaintext highlighter-rouge">make</code>, <code class="language-plaintext highlighter-rouge">libusb</code>, <code class="language-plaintext highlighter-rouge">hidapi</code>, <code class="language-plaintext highlighter-rouge">python</code>, and <code class="language-plaintext highlighter-rouge">openssl</code> stay put. Glance over its proposed list before confirming, and if you use something like <code class="language-plaintext highlighter-rouge">dfu-util</code> for non-QMK work, leave that one out of the uninstall.</li> </ul> <h3 id="other-gotchas-you-might-run-into">Other gotchas you might run into</h3> <p><strong><code class="language-plaintext highlighter-rouge">qmk: no such file or directory</code> right after uninstalling.</strong> This one is a classic head-scratcher. zsh caches the full path of each command it has run, so after you delete a binary it keeps reaching for the old, now-gone path. Clear the cache and you are fine:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">hash</span> <span class="nt">-r</span>
</code></pre></div></div> <p>(<code class="language-plaintext highlighter-rouge">rehash</code> works too, or just open a new terminal.)</p> <p><strong><code class="language-plaintext highlighter-rouge">qmk_home</code> set to <code class="language-plaintext highlighter-rouge">None</code>, so you are building the wrong tree.</strong> If the CLI does not know where the QMK source lives, it falls back to <code class="language-plaintext highlighter-rouge">~/qmk_firmware</code>, which might be empty or simply not the repo your keymap is in, and you will get baffling build results. Check it, then set it:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk config user.qmk_home
qmk config user.qmk_home<span class="o">=</span>/Users/you/opensource/qmk_firmware
</code></pre></div></div> <h2 id="quick-reference">Quick reference</h2> <p>Where everything lives by default:</p> <ul> <li><strong>CLI binary</strong>: <code class="language-plaintext highlighter-rouge">~/.local/bin/qmk</code>, a symlink into <code class="language-plaintext highlighter-rouge">~/.local/share/uv/tools/qmk/</code></li> <li><strong>Compilers and flashing tools</strong>: <code class="language-plaintext highlighter-rouge">~/Library/Application Support/qmk/</code></li> <li><strong>CLI config</strong>: <code class="language-plaintext highlighter-rouge">~/Library/Application Support/qmk/qmk.ini</code></li> <li><strong>QMK source</strong>: wherever <code class="language-plaintext highlighter-rouge">user.qmk_home</code> points</li> <li><strong>Keymap (External Userspace)</strong>: <code class="language-plaintext highlighter-rouge">&lt;overlay_dir&gt;/keyboards/&lt;vendor&gt;/&lt;board&gt;/keymaps/&lt;name&gt;/</code></li> <li><strong>Keymap (in-tree)</strong>: <code class="language-plaintext highlighter-rouge">&lt;qmk_home&gt;/keyboards/&lt;vendor&gt;/&lt;board&gt;/keymaps/&lt;name&gt;/</code></li> </ul> <p>Handy sanity commands:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qmk doctor
which qmk
qmk config
qmk compile <span class="nt">-kb</span> zsa/moonlander <span class="nt">-km</span> mykeymap
</code></pre></div></div>]]></content><author><name>Qing Wu</name></author><category term="qmk"/><category term="macos"/><category term="mechanical_keyboard"/><category term="homebrew"/><summary type="html"><![CDATA[A practical walkthrough for installing QMK on macOS the modern, self-contained way, and untangling an older Homebrew-based setup if you have one.]]></summary></entry><entry><title type="html">Installing Elixir Language Server for Vim</title><link href="https://www.wiserfirst.com/blog/installing-elixir-language-server-for-vim/" rel="alternate" type="text/html" title="Installing Elixir Language Server for Vim"/><published>2025-12-25T15:50:00+11:00</published><updated>2025-12-25T15:50:00+11:00</updated><id>https://www.wiserfirst.com/blog/installing-elixir-language-server-for-vim</id><content type="html" xml:base="https://www.wiserfirst.com/blog/installing-elixir-language-server-for-vim/"><![CDATA[<p>ElixirLS is the Elixir Language Server that provides IDE features like autocomplete, diagnostics, go-to-definition, and Dialyzer integration. This guide covers setting it up in Vim using coc.nvim.</p> <h2 id="prerequisites">Prerequisites</h2> <ul> <li>Vim or Neovim with <a href="https://github.com/neoclide/coc.nvim">coc.nvim</a> installed</li> <li>Elixir and Erlang installed (ideally managed via asdf)</li> </ul> <h2 id="step-1-install-elixirls">Step 1: Install ElixirLS</h2> <p>Clone and build ElixirLS:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/elixir-lsp/elixir-ls.git ~/.elixir-ls
<span class="nb">cd</span> ~/.elixir-ls
mix deps.get
<span class="nv">MIX_ENV</span><span class="o">=</span>prod mix compile
<span class="nv">MIX_ENV</span><span class="o">=</span>prod mix elixir_ls.release2 <span class="nt">-o</span> release
</code></pre></div></div> <p>The last command builds the executable language server to <code class="language-plaintext highlighter-rouge">~/.elixir-ls/release/</code>, and that is the path we need to use in the coc.nvim configuration.</p> <h2 id="step-2-configure-cocnvim">Step 2: Configure coc.nvim</h2> <p>Open your coc settings in Vim with <code class="language-plaintext highlighter-rouge">:CocConfig</code> and add:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"elixir.pathToElixirLS"</span><span class="p">:</span><span class="w"> </span><span class="s2">"~/.elixir-ls/release/language_server.sh"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <h2 id="step-3-install-coc-elixir">Step 3: Install coc-elixir</h2> <p>In Vim, run:</p> <div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">:</span>CocInstall coc<span class="p">-</span>elixir
</code></pre></div></div> <p>This installs the coc.nvim extension that communicates with ElixirLS.</p> <h2 id="step-4-verify">Step 4: Verify</h2> <p>Open an Elixir file in your project. You should see:</p> <ul> <li>Autocomplete suggestions</li> <li>Inline diagnostics and warnings</li> <li>Go-to-definition working (<code class="language-plaintext highlighter-rouge">:call CocAction('jumpDefinition')</code>)</li> </ul> <h2 id="understanding-the-elixir_ls-directory">Understanding the .elixir_ls Directory</h2> <p>When you first open an Elixir project, ElixirLS creates a <code class="language-plaintext highlighter-rouge">.elixir_ls/</code> directory and compiles your project. You might wonder why it doesn’t just use <code class="language-plaintext highlighter-rouge">_build/</code>.</p> <p>ElixirLS maintains its own build for several reasons:</p> <ol> <li> <p><strong>Avoids conflicts</strong> - Your <code class="language-plaintext highlighter-rouge">_build/</code> uses your development environment. ElixirLS compiles separately so running <code class="language-plaintext highlighter-rouge">mix test</code> doesn’t invalidate the language server’s build.</p> </li> <li> <p><strong>Different compile options</strong> - ElixirLS uses flags that capture additional metadata for IDE features. These would slow down normal development builds.</p> </li> <li> <p><strong>Dialyzer PLT files</strong> - ElixirLS caches Dialyzer’s Persistent Lookup Table here. Building PLTs is slow, so caching them per-project saves time.</p> </li> <li> <p><strong>Independence</strong> - If your <code class="language-plaintext highlighter-rouge">_build/</code> breaks, ElixirLS keeps working.</p> </li> </ol> <p>Consider adding <code class="language-plaintext highlighter-rouge">.elixir_ls/</code> to your global gitignore:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">".elixir_ls/"</span> <span class="o">&gt;&gt;</span> ~/.gitignore_global
git config <span class="nt">--global</span> core.excludesfile ~/.gitignore_global
</code></pre></div></div> <h2 id="troubleshooting">Troubleshooting</h2> <p>If ElixirLS behaves unexpectedly, the simplest fix is to delete its cache and restart:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">rm</span> <span class="nt">-rf</span> .elixir_ls/
</code></pre></div></div> <p>Then in Vim:</p> <div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">:</span>CocRestart
</code></pre></div></div> <p>ElixirLS automatically recreates the directory and recompiles when it restarts.</p> <p><strong>Common reasons to clear the cache:</strong></p> <ul> <li>Upgraded Elixir or Erlang versions</li> <li>Dialyzer giving stale warnings</li> <li>Language server stuck or unresponsive</li> <li>Dependency changes not being picked up</li> </ul>]]></content><author><name>Qing Wu</name></author><category term="elixir"/><category term="lsp"/><category term="vim"/><summary type="html"><![CDATA[Leveraging the power of LSP]]></summary></entry><entry><title type="html">Taking Control of Terminal Titles</title><link href="https://www.wiserfirst.com/blog/taking-control-of-terminal-titles/" rel="alternate" type="text/html" title="Taking Control of Terminal Titles"/><published>2025-08-24T21:55:00+10:00</published><updated>2025-08-24T21:55:00+10:00</updated><id>https://www.wiserfirst.com/blog/taking-control-of-terminal-titles</id><content type="html" xml:base="https://www.wiserfirst.com/blog/taking-control-of-terminal-titles/"><![CDATA[<h2 id="the-background">The Background</h2> <p>As a developer, I use the terminal extensively and often SSH into remote machines for management tasks. When I connect via SSH, iTerm2 automatically updates both the window and tab titles to something like <code class="language-plaintext highlighter-rouge">user@machine-ip:directory-name</code>, which provides helpful context about where I’m working. The problem? These titles don’t automatically revert when I disconnect, leaving me with stale information in my terminal tabs.</p> <p>For years, I had no idea what was happening under the hood, let alone how to fix it. My workaround was crude but effective: close the tab and open a new one. Eventually, I got fed up and decided to take some actions. After some research, I added this mysterious snippet to my <code class="language-plaintext highlighter-rouge">.zshrc</code>:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>precmd<span class="o">()</span> <span class="o">{</span>
  <span class="c"># sets the tab title to current dir</span>
  <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\e</span><span class="s2">]1;</span><span class="k">${</span><span class="nv">PWD</span><span class="p">##*/</span><span class="k">}</span><span class="se">\a</span><span class="s2">"</span>
<span class="o">}</span>
</code></pre></div></div> <p>I’ll be honest—I didn’t fully understand how it worked, but it did the job. This function “forced” my tab title to always display the current directory name, even after disconnecting from SSH sessions.</p> <p>Later, I discovered <a href="https://github.com/sorin-ionescu/prezto">Prezto</a>, a configuration framework for Zsh that includes a handy <code class="language-plaintext highlighter-rouge">set-tab-title</code> function. This gave me more flexible control over tab titles, so I happily retired my hacky <code class="language-plaintext highlighter-rouge">precmd</code> solution.</p> <p>You might have noticed that the window title remained untouched throughout this journey. It simply wasn’t annoying enough to warrant investigation—until Claude Code came along.</p> <p>Since its public launch in February, Claude Code has become my go-to tool for AI-assisted coding. Like SSH, it helpfully sets custom tab and window titles to show what it’s working on. Also like SSH, these titles stick around after Claude Code exits, cluttering my terminal with outdated information like “Building Feature X…” or “Optimising test performance…” long after those tasks completed.</p> <p>Since I was using Claude Code across multiple projects every day, these stale titles were starting to get on my nerves and that was the final straw. It was time to properly understand how terminal titles work and build a comprehensive solution.</p> <h2 id="understanding-terminal-titles-in-iterm2">Understanding Terminal Titles in iTerm2</h2> <p>Before diving into solutions, let’s understand what we’re dealing with. iTerm2 (and most terminal emulators) recognize two distinct title types:</p> <ol> <li><strong>Tab Title</strong> - Displayed on the individual tab</li> <li><strong>Window Title</strong> - Shown in the center of the window’s title bar</li> </ol> <p>These titles are controlled through ANSI escape sequences - special character combinations that terminals interpret as commands rather than text to display.</p> <h3 id="the-magic-behind-terminal-titles">The Magic Behind Terminal Titles</h3> <p>Here’s how escape sequences work for setting titles:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Set window title only (shows in title bar)</span>
<span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]2;Your Window Title</span><span class="se">\0</span><span class="s2">07"</span>

<span class="c"># Set tab title only (shows in tab)</span>
<span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]1;Your Tab Title</span><span class="se">\0</span><span class="s2">07"</span>

<span class="c"># Set both window and tab to the same title</span>
<span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]0;Your Title</span><span class="se">\0</span><span class="s2">07"</span>
</code></pre></div></div> <p>The cryptic <code class="language-plaintext highlighter-rouge">\033]</code> starts the escape sequence, the number (0, 1, or 2) specifies which title to set, and <code class="language-plaintext highlighter-rouge">\007</code> (the bell character) ends it.</p> <p>Quick note on escape characters: You might see these written as <code class="language-plaintext highlighter-rouge">\e</code> (instead of <code class="language-plaintext highlighter-rouge">\033</code>) and <code class="language-plaintext highlighter-rouge">\a</code> (instead of <code class="language-plaintext highlighter-rouge">\007</code>) in some scripts—they’re equivalent, just different representations of the same ASCII characters. I’m using the octal format (<code class="language-plaintext highlighter-rouge">\033</code> and <code class="language-plaintext highlighter-rouge">\007</code>) here for maximum compatibility across different shells and systems.</p> <p>Simple, right? Well, not exactly intuitive and quite cumbersome to use, which is why we’re building a better interface.</p> <h2 id="the-solution-a-simple-title-manager">The Solution: A Simple Title Manager</h2> <p>Let’s create a user-friendly function that makes title management a breeze. Add this to your <code class="language-plaintext highlighter-rouge">~/.zshrc</code> (or <code class="language-plaintext highlighter-rouge">~/.bashrc</code> for Bash users):</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal title management functions</span>
terminal_titles<span class="o">()</span> <span class="o">{</span>
    <span class="k">case</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">in
        </span>window<span class="p">)</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]2;</span><span class="nv">$2</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="p">;;</span>
        tab<span class="p">)</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]1;</span><span class="nv">$2</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="p">;;</span>
        both<span class="p">)</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]0;</span><span class="nv">$2</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="p">;;</span>
        <span class="nb">help</span><span class="p">)</span>
            <span class="nb">echo</span> <span class="s2">"Usage: terminal_titles [window|tab|both|reset|help] [title]"</span>
            <span class="nb">echo</span> <span class="s2">"  window [title] - Set window title only"</span>
            <span class="nb">echo</span> <span class="s2">"  tab [title]    - Set tab title only"</span>
            <span class="nb">echo</span> <span class="s2">"  both [title]   - Set both window and tab to same title"</span>
            <span class="nb">echo</span> <span class="s2">"  reset          - Reset to default titles"</span>
            <span class="nb">echo</span> <span class="s2">"  help           - Show this help message"</span>
            <span class="nb">echo</span> <span class="s2">"  (no args)      - Same as reset"</span>
            <span class="p">;;</span>
        reset|<span class="s2">""</span><span class="p">)</span>
            <span class="c"># Window title: full path with ~ for home</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]2;</span><span class="k">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="k">}</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="c"># Tab title: last portion of the full path</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]1;</span><span class="k">${</span><span class="nv">PWD</span><span class="p">##*/</span><span class="k">}</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="p">;;</span>
        <span class="k">*</span><span class="p">)</span>
            <span class="nb">echo</span> <span class="s2">"Unknown command: </span><span class="nv">$1</span><span class="s2">"</span>
            <span class="nb">echo</span> <span class="s2">"Use 'terminal_titles help' for usage"</span>
            <span class="p">;;</span>
    <span class="k">esac</span>
<span class="o">}</span>

<span class="c"># Alias for convenience</span>
<span class="nb">alias </span><span class="nv">tt</span><span class="o">=</span><span class="s1">'terminal_titles'</span>
</code></pre></div></div> <h3 id="how-to-use-it">How to Use It</h3> <p>Now you have a simple <code class="language-plaintext highlighter-rouge">tt</code> command at your disposal:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Reset titles to show current directory info</span>
tt

<span class="c"># Set a custom window title</span>
tt window <span class="s2">"Development Server"</span>

<span class="c"># Set a custom tab title</span>
tt tab <span class="s2">"logs"</span>

<span class="c"># Set both to the same title</span>
tt both <span class="s2">"Project X"</span>

<span class="c"># Get help</span>
tt <span class="nb">help</span>
</code></pre></div></div> <p>The default behavior (<code class="language-plaintext highlighter-rouge">tt</code> with no arguments) resets your titles to something useful:</p> <ul> <li><strong>Window title</strong>: Full path of current directory (with <code class="language-plaintext highlighter-rouge">~</code> for home)</li> <li><strong>Tab title</strong>: Just the current folder name</li> </ul> <p>For example, if you’re in <code class="language-plaintext highlighter-rouge">/Users/you/projects/awesome-app</code>, the window shows <code class="language-plaintext highlighter-rouge">~/projects/awesome-app</code> and the tab shows <code class="language-plaintext highlighter-rouge">awesome-app</code>.</p> <h2 id="conclusion">Conclusion</h2> <p>After years of closing and reopening tabs to clear stale SSH titles, and more recently dealing with Claude Code’s persistent status messages, I finally have a solution that just works. The <code class="language-plaintext highlighter-rouge">tt</code> command is now muscle memory—quick to type, easy to remember, and does exactly what I need.</p> <p>The beauty of this solution is its simplicity. No complex configurations, no heavyweight terminal managers—just a few lines of shell script that give you complete control. Whether you’re jumping between SSH sessions, running Claude Code throughout the day, or just want cleaner tab organization, you now have the tools to keep your terminal titles under control.</p> <p>Happy terminal customizing! 🚀</p> <hr/> <blockquote> <p><strong>Note</strong>: After building this solution, I discovered that Prezto actually has a <code class="language-plaintext highlighter-rouge">set-window-title</code> command hiding alongside the <code class="language-plaintext highlighter-rouge">set-tab-title</code> function I’d been using for years. Had I found it earlier, this entire journey into the depths of ANSI escape sequences might never have happened. But you know what? I’m okay with missing it. Sometimes the scenic route teaches you more than the shortcut ever could, and now I truly understand what’s happening under the hood rather than just blindly using a command.</p> </blockquote> <h2 id="appendix-auto-reset-with-smart-persistence">Appendix: Auto-Reset with Smart Persistence</h2> <p>If you want titles to automatically reset after commands like Claude Code exit, but keep your manually-set titles, here’s an enhanced version with intelligent auto-reset. This adds more complexity but offers finer control:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal title management functions with smart auto-reset</span>
terminal_titles<span class="o">()</span> <span class="o">{</span>
    <span class="k">case</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">in
        </span>window<span class="p">)</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]2;</span><span class="nv">$2</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="nb">export </span><span class="nv">TERMINAL_TITLE_MANUAL</span><span class="o">=</span>1
            <span class="p">;;</span>
        tab<span class="p">)</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]1;</span><span class="nv">$2</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="nb">export </span><span class="nv">TERMINAL_TITLE_MANUAL</span><span class="o">=</span>1
            <span class="p">;;</span>
        both<span class="p">)</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]0;</span><span class="nv">$2</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="nb">export </span><span class="nv">TERMINAL_TITLE_MANUAL</span><span class="o">=</span>1
            <span class="p">;;</span>
        <span class="nb">help</span><span class="p">)</span>
            <span class="nb">echo</span> <span class="s2">"Usage: terminal_titles [window|tab|both|reset|auto|help] [title]"</span>
            <span class="nb">echo</span> <span class="s2">"  window [title] - Set window title only"</span>
            <span class="nb">echo</span> <span class="s2">"  tab [title]    - Set tab title only"</span>
            <span class="nb">echo</span> <span class="s2">"  both [title]   - Set both window and tab to same title"</span>
            <span class="nb">echo</span> <span class="s2">"  reset          - Reset to default titles"</span>
            <span class="nb">echo</span> <span class="s2">"  auto           - Enable auto-reset after commands"</span>
            <span class="nb">echo</span> <span class="s2">"  help           - Show this help message"</span>
            <span class="nb">echo</span> <span class="s2">"  (no args)      - Same as reset"</span>
            <span class="p">;;</span>
        auto<span class="p">)</span>
            <span class="nb">unset </span>TERMINAL_TITLE_MANUAL
            terminal_titles reset
            <span class="nb">echo</span> <span class="s2">"Auto-reset enabled"</span>
            <span class="p">;;</span>
        reset|<span class="s2">""</span><span class="p">)</span>
            <span class="c"># Window title: full path with ~ for home</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]2;</span><span class="k">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="k">}</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="c"># Tab title: last portion of the full path</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]1;</span><span class="k">${</span><span class="nv">PWD</span><span class="p">##*/</span><span class="k">}</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="nb">unset </span>TERMINAL_TITLE_MANUAL
            <span class="p">;;</span>
        <span class="k">*</span><span class="p">)</span>
            <span class="nb">echo</span> <span class="s2">"Unknown command: </span><span class="nv">$1</span><span class="s2">"</span>
            <span class="nb">echo</span> <span class="s2">"Use 'terminal_titles help' for usage"</span>
            <span class="p">;;</span>
    <span class="k">esac</span>
<span class="o">}</span>

<span class="c"># Alias for convenience</span>
<span class="nb">alias </span><span class="nv">tt</span><span class="o">=</span><span class="s1">'terminal_titles'</span>

<span class="c"># Auto-reset after commands ONLY if not manually set (for zsh)</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$ZSH_VERSION</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
    </span>precmd<span class="o">()</span> <span class="o">{</span>
        <span class="c"># Only reset if titles weren't manually set</span>
        <span class="k">if</span> <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$TERMINAL_TITLE_MANUAL</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
            <span class="c"># Window title: full path with ~ for home</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]2;</span><span class="k">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="k">}</span><span class="se">\0</span><span class="s2">07"</span>
            <span class="c"># Tab title: last portion of the full path</span>
            <span class="nb">echo</span> <span class="nt">-ne</span> <span class="s2">"</span><span class="se">\0</span><span class="s2">33]1;</span><span class="k">${</span><span class="nv">PWD</span><span class="p">##*/</span><span class="k">}</span><span class="se">\0</span><span class="s2">07"</span>
        <span class="k">fi</span>
    <span class="o">}</span>
<span class="k">fi</span>
</code></pre></div></div> <p>This enhanced version adds:</p> <ul> <li><strong>Smart persistence</strong>: Manually-set titles survive command execution</li> <li><strong>Auto-reset mode</strong>: Titles automatically update after commands (when not manually set)</li> <li><strong>Flexible control</strong>: Switch between manual and automatic modes with <code class="language-plaintext highlighter-rouge">tt auto</code></li> </ul> <h3 id="the-smart-workflow">The Smart Workflow</h3> <ol> <li><strong>Normal operation</strong>: Titles auto-update to show your current directory</li> <li><strong>Set custom title</strong>: <code class="language-plaintext highlighter-rouge">tt tab "server logs"</code> - This persists across commands</li> <li><strong>Return to auto-mode</strong>: <code class="language-plaintext highlighter-rouge">tt auto</code> or <code class="language-plaintext highlighter-rouge">tt reset</code></li> </ol>]]></content><author><name>Qing Wu</name></author><category term="iTerm2"/><category term="terminal"/><summary type="html"><![CDATA[One simple command to rule them all]]></summary></entry><entry><title type="html">The Case of Rejected Certificates: An OTP Mystery</title><link href="https://www.wiserfirst.com/blog/bug-in-programming-language/" rel="alternate" type="text/html" title="The Case of Rejected Certificates: An OTP Mystery"/><published>2025-03-08T22:30:00+11:00</published><updated>2025-03-08T22:30:00+11:00</updated><id>https://www.wiserfirst.com/blog/bug-in-programming-language</id><content type="html" xml:base="https://www.wiserfirst.com/blog/bug-in-programming-language/"><![CDATA[<h2 id="the-problem-arises">The Problem Arises</h2> <p>A couple of weeks ago, we attempted to upgrade our Erlang/OTP version from 24 to 25.3.2.16, which was the latest release at the time. Unfortunately, shortly after the new release containing this change was deployed to production, our Customer Service team reported that a specific payment feature had stopped working. In fact, they noticed that we had stopped receiving this type of payment almost immediately after the new release hit production. The timing was too suspiciously close for this to be a coincidence.</p> <h2 id="the-investigation">The Investigation</h2> <p>When investigating this issue, I had no idea what the cause was, but I did have significant time pressure due to the nature of the problem—payments not being processed is always urgent!</p> <p>First thing I did was to understand where the failure was happening and managed to replicate it in my local environment. Next, I methodically went through the all the changes in this release, reverting suspicious-looking changes one by one. Surprisingly, none of our actual code changes was the culprit.</p> <p>The OTP version upgrade had seemed like one of the most innocent changes with regard to the payment issue we were facing. However, after exhausting other possibilities, I tested against OTP 24 since the OTP upgrade was a relatively major change in the same release. I was quite shocked to discover that the new version of OTP was indeed the guilty party.</p> <h2 id="the-partial-solution">The (Partial) Solution</h2> <p>Since I’m not an expert on certificate validation in Erlang, the error message we got when making requests to the bank looks cryptic:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TLS :client: In state :wait_cert_cr at ssl_handshake.erl:2123 generated CLIENT ALERT: Fatal - Unsupported Certificate
 - {:key_usage_mismatch,
 { {:Extension, {2, 5, 29, 15}, true, [:keyCertSign, :cRLSign]},
  {:Extension, {2, 5, 29, 37}, false,
   [{1, 3, 6, 1, 5, 5, 7, 3, 2}, {1, 3, 6, 1, 5, 5, 7, 3, 1}]}}}
</code></pre></div></div> <p>But armed with this error message, I was able to find a <a href="https://github.com/erlang/otp/issues/9208">Github issue</a> in the official OTP repository about the same problem. Apparently other developers making HTTP requests with Erlang/Elixir had encountered the same issue.</p> <p>Thanks to Ingela Andin, the maintainer, and the community’s efforts, a fix had already been released for OTP 26 and 27. But unfortunately for us, there was an impression that OTP 25 wasn’t affected, so no fix had been done for it. Given our urgent situation, we decided to revert back to OTP 24 to restore payment processing as quickly as possible.</p> <p>It’s worth noting that after I reported that OTP 25 was indeed affected by the same issue, Ingela responded quickly and worked on backporting the fix. A new patch version with the fix was released about two weeks ago, clearing our path to safely upgrading to OTP 25.</p> <p>Now that we had a solution, I wanted to better understand what caused the problem in the first place.</p> <h2 id="understanding-digital-certificates">Understanding Digital Certificates</h2> <p>To understand the bug, we need a quick primer on SSL/TLS certificates: digital certificates are like digital ID cards that websites use to prove their identity. Each certificate contains:</p> <ul> <li>The website’s public key</li> <li>Information about the website (domain name, etc.)</li> <li>Information about how the certificate can be used</li> <li>A signature from a trusted Certificate Authority (CA)</li> </ul> <p>Certificates have “extensions” that specify what they can be used for. Two important ones are:</p> <ul> <li>Key Usage (KU): Broadly defines what the certificate’s key can do (sign things, encrypt things, etc.)</li> <li>Extended Key Usage (EKU): More specifically defines the certificate’s purpose (web server authentication, email, etc.)</li> </ul> <h2 id="the-bug-in-otp">The Bug in OTP</h2> <p>The bug occurred because recent versions of OTP was enforcing a rule that wasn’t actually specified in the certificate standards (RFC 5280).</p> <p>In simple terms:</p> <ul> <li>The certificates from certain CAs like Entrust had a flag set indicating they could sign other certificates (keyCertSign)</li> <li>They also had flags set saying they could be used for web server authentication</li> <li>OTP thought these two purposes were contradictory and rejected the certificate</li> </ul> <p>It’s like if you’re qualified as both a teacher and a restaurant chef, but then a bureaucrat refused to accept because “you can’t possibly do both these unrelated jobs.” In reality, of course, there’s no reason someone couldn’t be qualified for both roles independently.</p> <p>And same goes for digital certificates. The certificate standard (RFC 5280) allows certificates to serve multiple purposes simultaneously, but OTP’s new validation logic was too restrictive.</p> <p>For those interested in further technical details, there are extensive discussions in the <a href="https://github.com/erlang/otp/issues/9208">Github issue</a> and here is the <a href="https://github.com/erlang/otp/pull/9286">PR</a> that fixed it.</p> <h2 id="takeaways">Takeaways</h2> <p>A few interesting lessons from this experience:</p> <ol> <li>Hidden Complexity: Even mature, well-tested software like Erlang/OTP can have subtle bugs in complex areas like SSL/TLS.</li> <li>Implementation vs. Specification: The bug wasn’t a coding error but an overly strict interpretation of a technical standard.</li> <li>Community Matters: Thanks to the Erlang community for identifying and fixing this issue very quickly.</li> </ol> <h2 id="summary">Summary</h2> <p>In this post, we started with an unexpected payment issue in production from upgrading the OTP version to 25. After identifying the new OTP version as the culprit, we had to revert back to OTP 24.</p> <p>We also dove into understanding how the bug happened, which was essentially an overly strict interpretation of certificate standards. Thanks to the responsive Erlang community and OTP maintainers, a fix was backported to OTP 25, resolving the bug.</p> <p>For me this was quite an interesting experience, because the overwhelming majority of bugs we face as developers are introduced by ourselves in the application layer. Sometimes we do encounter bugs in the library or framework that we use, but that’s pretty rare. It is ultra rare to face a bug in the underlying programming language. In fact, this was the very first one I had in my whole career as a developer, and I’ve been doing this for almost 20 years.</p>]]></content><author><name>Qing Wu</name></author><category term="Erlang"/><category term="OTP"/><summary type="html"><![CDATA[Tracking down a production issue to a subtle bug in Erlang/OTP]]></summary></entry><entry><title type="html">Understanding Application Configuration in Elixir</title><link href="https://www.wiserfirst.com/blog/understand-configs-in-elixir/" rel="alternate" type="text/html" title="Understanding Application Configuration in Elixir"/><published>2024-04-18T22:19:00+10:00</published><updated>2024-04-18T22:19:00+10:00</updated><id>https://www.wiserfirst.com/blog/understand-configs-in-elixir</id><content type="html" xml:base="https://www.wiserfirst.com/blog/understand-configs-in-elixir/"><![CDATA[<p>Recently I started working on an Elixir project, which was a breath of fresh air, especially after two years mainly working with Node. However, it didn’t take long before I stumbled upon something that puzzled me.</p> <p>In the production configuration files of this project, we have environment variable names interpolated in strings as follows:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/prod.exs</span>
<span class="n">config</span> <span class="ss">:my_app</span><span class="p">,</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Repo</span><span class="p">,</span>
  <span class="ss">hostname:</span> <span class="s2">"${DATABASE_HOSTNAME}"</span><span class="p">,</span>
  <span class="ss">username:</span> <span class="s2">"${DATABASE_USERNAME}"</span><span class="p">,</span>
  <span class="o">...</span>
</code></pre></div></div> <p>Clearly this approach wouldn’t work for the <code class="language-plaintext highlighter-rouge">:dev</code> environment, where we would typically see something like this:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/dev.exs</span>
<span class="n">config</span> <span class="ss">:my_app</span><span class="p">,</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Repo</span><span class="p">,</span>
  <span class="ss">hostname:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">"DATABASE_HOSTNAME"</span><span class="p">),</span>
  <span class="ss">username:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">"DATABASE_USERNAME"</span><span class="p">),</span>
  <span class="o">...</span>
</code></pre></div></div> <p>However, I also knew that the interpolation syntax did work for us in production. This made me curious: How exactly does this work? It was time for some investigation.</p> <h2 id="the-how">The How</h2> <p>After consulting with our old friend Google and chatting with our new friend GPT-4, I learnt that this peculiar method of setting configuration values from environment variables is facilitated by Distillery, the tool we use for building releases.</p> <p>For this functionality to be enabled, the environment variable <code class="language-plaintext highlighter-rouge">REPLACE_OS_VARS</code> must be set to <code class="language-plaintext highlighter-rouge">true</code>. And then Distillery would replace environment variables with their actual values upon encountering interpolation syntax, such as <code class="language-plaintext highlighter-rouge">"${DATABASE_USERNAME}"</code>, in the configuration files.</p> <p>As a matter of fact, when it comes to handling application configuration, Distillery supports more than simple variable interpolation. It also supports Config Providers, which are custom modules capable of dynamically generating configuration at the start of your application. For further details, please refer to the relevant section in <a href="https://hexdocs.pm/distillery/config/runtime.html">Distillery documentation</a>.</p> <h2 id="the-why">The Why</h2> <p>While it’s enlightening to learn how it works, I kept wondering: why does Distillery offer support for both interpolation and Config Providers? Isn’t <code class="language-plaintext highlighter-rouge">System.get_env</code> good enough for this purpose? As it turns out, although <code class="language-plaintext highlighter-rouge">System.get_env</code> is quite effective for the <code class="language-plaintext highlighter-rouge">:dev</code> environment, it can be inadequate for the production environment in many cases.</p> <p>To grasp this, we need to take a step back and have a quick review of how Elixir the language works. Elixir is a compiled language. When you execute <code class="language-plaintext highlighter-rouge">mix compile</code>, the Elixir compiler transforms your project into Erlang bytecode. This bytecode then runs on the Erlang Virtual Machine, commonly known as Bogdan/Björn’s Erlang Abstract Machine (BEAM).</p> <p>The configuration files and code that are outside of function bodies within a module are evaluated at compile-time. Thus, if you use <code class="language-plaintext highlighter-rouge">System.get_env("DATABASE_HOSTNAME")</code> within a configuration file, it’s evaluated during the compilation process. This means the value of the environment variable <code class="language-plaintext highlighter-rouge">DATABASE_HOSTNAME</code> is then captured and embedded in the application as a static value.</p> <p>For production configurations, the goal is generally to dynamically capture runtime environment variables, rather than to embedding those from the build server as static values. Apparently using <code class="language-plaintext highlighter-rouge">System.get_env</code> wouldn’t achieve this purpose.</p> <p>That is precisely why Distillery supports both the interpolation syntax and Config Providers, enabling dynamic configuration value setting from the runtime environment of the application.</p> <p>It’s noteworthy that with version 1.9, Elixir introduced built-in support for <a href="https://hexdocs.pm/mix/Mix.Tasks.Release.html">releases</a> as well, which includes a novel approach to runtime configuration. This was achieved through the introduction of a new configuration file <code class="language-plaintext highlighter-rouge">config/releases.exs</code>, which is executed every time a release starts. Then in Elixir 1.11, another configuration file <code class="language-plaintext highlighter-rouge">config/runtime.exs</code> was introduced, which is essentially a superior version of <code class="language-plaintext highlighter-rouge">config/releases.exs</code>. For more details on these developments, please consult to the <a href="https://hexdocs.pm/elixir/1.11.0/changelog.html#config-runtime-exs-and-mix-app-config">relevant documentation</a>.</p> <h2 id="summary">Summary</h2> <p>In this post, I shared my initial confusion about the interpolation syntax <code class="language-plaintext highlighter-rouge">"${DATABASE_HOSTNAME}"</code> in our production configuration files, followed by an exploration into understanding how it works and why is it necessary for runtime configuration. As always, my aim is to provide a reference for my further self and perhaps assist others along their journey.</p>]]></content><author><name>Qing Wu</name></author><category term="elixir"/><category term="configuration"/><category term="distillery"/><category term="release"/><summary type="html"><![CDATA[Application configuration can get interesting in Elixir. Let's dig ...]]></summary></entry><entry><title type="html">Type Narrowing with Custom Functions in Typescript</title><link href="https://www.wiserfirst.com/blog/type-narrowing-with-custom-functions-in-typescript/" rel="alternate" type="text/html" title="Type Narrowing with Custom Functions in Typescript"/><published>2023-02-12T23:30:00+11:00</published><updated>2023-02-12T23:30:00+11:00</updated><id>https://www.wiserfirst.com/blog/type-narrowing-with-custom-functions-in-typescript</id><content type="html" xml:base="https://www.wiserfirst.com/blog/type-narrowing-with-custom-functions-in-typescript/"><![CDATA[<p>Typescript has union types. For example:</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">type</span> <span class="nx">StringOrNumber</span> <span class="o">=</span> <span class="kr">string</span> <span class="o">|</span> <span class="kr">number</span>
</code></pre></div></div> <p>Here the <code class="language-plaintext highlighter-rouge">StringOrNumber</code> type can be either <code class="language-plaintext highlighter-rouge">string</code> or <code class="language-plaintext highlighter-rouge">number</code> as expected. The member types do not have to be primitive types and they can be custom types as well.</p> <p>When dealing with union types, before we can apply operations or functions that are specific to one member type, we would need to figure out which member type we have and that’s called type narrowing.</p> <p>There are many ways to narrow the type, but in this post I specifically want to cover the syntax for creating a custom function to do type narrowing. I found myself referring back to the “Using type predicates” section on <a href="https://www.typescriptlang.org/docs/handbook/2/narrowing.html">the Narrowing page in Typescript documentation</a>, so I figure it might be a good idea to make it slightly easier to find and potentially easier to remember by writing a post about it.</p> <p>Let’s say we want to create a type narrowing function for the <code class="language-plaintext highlighter-rouge">StringOrNumber</code> type above, it would look like this:</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">isString</span><span class="p">(</span><span class="nx">input</span><span class="p">:</span> <span class="nx">StringOrNumber</span><span class="p">):</span> <span class="nx">input</span> <span class="k">is</span> <span class="kr">string</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">typeof</span> <span class="nx">input</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span>
<span class="p">}</span>
</code></pre></div></div> <p>Here, <code class="language-plaintext highlighter-rouge">input is string</code> is called a type predicate and it is the return type of the <code class="language-plaintext highlighter-rouge">isString</code> function. In the function body, it needs to return a boolean value: <code class="language-plaintext highlighter-rouge">true</code> means <code class="language-plaintext highlighter-rouge">input</code> is the expected type and <code class="language-plaintext highlighter-rouge">false</code> means it is not. The function’s logic can be as complex as it needs to be. As long as it returns a boolean value in the end, it’ll work fine as a type narrowing function or say a type guard.</p> <p>In this example, returning <code class="language-plaintext highlighter-rouge">true</code> means <code class="language-plaintext highlighter-rouge">input</code> is a string, otherwise it’s a number, considering it can only be either a string or a number.</p> <p>We can use it like the following:</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// assuming we have a function that returns a string or a number</span>
<span class="kd">let</span> <span class="nx">a</span><span class="p">:</span> <span class="nx">StringOrNumber</span> <span class="o">=</span> <span class="nf">getStringOrNumber</span><span class="p">()</span>

<span class="k">if </span><span class="p">(</span><span class="nf">isString</span><span class="p">(</span><span class="nx">a</span><span class="p">))</span> <span class="p">{</span>
  <span class="nx">a</span><span class="p">.</span><span class="nx">length</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="nx">a</span> <span class="o">+</span> <span class="mi">3</span>
<span class="p">}</span>
</code></pre></div></div> <p>Of course, this is just a contrived example to demonstrate the correct syntax. In a real world application, we would most likely just do <code class="language-plaintext highlighter-rouge">typeof input === 'string'</code> instead of creating a function to do it.</p> <p>One more thing worth noting is that when we create a custom type guard, Typescript would treat us as adults and completely trust that it will do the right thing. For example, we can define a nonsensical type guard like this:</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">isString</span><span class="p">(</span><span class="nx">input</span><span class="p">:</span> <span class="nx">StringOrNumber</span><span class="p">):</span> <span class="nx">input</span> <span class="k">is</span> <span class="kr">string</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">typeof</span> <span class="nx">input</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">number</span><span class="dl">'</span>
<span class="p">}</span>
</code></pre></div></div> <p>And Typescript would happily accept it and gives no compiler error or warning, but since it’s narrowing the type incorrectly, some runtime errors should be expected after using this type guard.</p> <p>In this post, I briefly talked about what type narrowing is and then looked into how to define our own type guard. As usual, the hope is to help my future self and potentially be of service to other people who are starting their Typescript journey as well.</p>]]></content><author><name>Qing Wu</name></author><category term="Typescript"/><category term="type-narrowing"/><category term="tiny-tips"/><summary type="html"><![CDATA[Typescript has union types and there are many ways to narrow the types]]></summary></entry><entry><title type="html">A Different Perspective for Elixir Macros</title><link href="https://www.wiserfirst.com/blog/different-perspective-for-elixir-macros/" rel="alternate" type="text/html" title="A Different Perspective for Elixir Macros"/><published>2022-03-21T11:48:00+11:00</published><updated>2022-03-21T11:48:00+11:00</updated><id>https://www.wiserfirst.com/blog/different-perspective-for-elixir-macros</id><content type="html" xml:base="https://www.wiserfirst.com/blog/different-perspective-for-elixir-macros/"><![CDATA[<p>In Elixir, macros are a way of metaprogramming, which is sometimes explained as something like “using code to write code”. While offering good intuition on what metaprogramming is about, it’s not very accurate, because it doesn’t actually write code.</p> <p>When trying to understand macros, I use the slightly different mental model: a macro is like a template with a name that can optionally have parameters; when the name is used, it’s substituted by the template with “values” of the arguments injected.</p> <p>Sorry if it didn’t make it better. I’ll try to explain what do I mean by that. But let’s first review how C macros work.</p> <h2 id="c-macos">C Macos</h2> <p>Before actually compiling a C program, the C compiler will use the C preprocessor to transform the program, which is also referred to as preprocessing. One of the things that happens in this preprocessing step is macro expansion.</p> <p>In GNU’s online documentation for <a href="https://gcc.gnu.org/onlinedocs/cpp/Macros.html">the C Preprocessor</a>, a macro is defined as following:</p> <blockquote> <p>A macro is a fragment of code which has been given a name. Whenever the name is used, it is replaced by the contents of the macro.</p> </blockquote> <p>You can define a macro like this:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define DOUBLE(x) (2 * x)
</span></code></pre></div></div> <blockquote> <p>NOTE: by convention macro names in C use uppercase.</p> </blockquote> <p>And then use it as below:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DOUBLE</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
</code></pre></div></div> <p>During preprocessing, the preprocessor will replace it with <code class="language-plaintext highlighter-rouge">(2 * x)</code>. The compiler would just see <code class="language-plaintext highlighter-rouge">(2 * x)</code> as if you wrote it in stead of <code class="language-plaintext highlighter-rouge">DOUBLE(5)</code> in the first place.</p> <p>So a macro in C allows us to define a fragment of code that can have parameters, and when it’s used the macro name would be replaced by the code fragment we defined with arguments interpolated.</p> <p>It’s worth noting that since this happens before compilation, the program is still just a piece of text, so both arguments interpolation and macro expansion are just literal text substitution.</p> <p>How does this relate to Elixir’s macros? Well macro expansion in Elixir is definitely not text substitution, but it’s still substitution, just happens at a higher level.</p> <h2 id="the-abstract-syntax-tree-ast">The Abstract Syntax Tree (AST)</h2> <p>Most programming languages have the Abstract Syntax Tree (AST), which is a tree structure the compiler builds from the source code before turning it into either machine code or byte code.</p> <p>In most languages, the AST is not exposed to us developers and we can get our code working without worrying about the AST or even knowing about it.</p> <p>In the case of Elixir, the compiler give us access to the AST. This comes with great power and allows us to do many things that aren’t possible in other languages, creating macros among them.</p> <p>You can get the AST for a piece of code by using <code class="language-plaintext highlighter-rouge">quote</code>, for example:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iex</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">&gt;</span> <span class="kn">quote</span> <span class="k">do</span>
<span class="o">...</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">&gt;</span> <span class="mi">1</span> <span class="o">+</span> <span class="mi">2</span>
<span class="o">...</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">&gt;</span> <span class="k">end</span>
<span class="p">{</span><span class="ss">:+</span><span class="p">,</span> <span class="p">[</span><span class="ss">context:</span> <span class="no">Elixir</span><span class="p">,</span> <span class="kn">import</span><span class="p">:</span> <span class="no">Kernel</span><span class="p">],</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">]}</span>
</code></pre></div></div> <p>This is probably the simplest form, but in general Elixir’s AST is represented as a three elements tuple. When the expression is more complex, its corresponding AST is usually deeply nested.</p> <p>In Elixir, the AST is also known as quoted expressions.</p> <p>For more details on working with quoted expressions, please refer to offical <a href="https://elixir-lang.org/getting-started/meta/quote-and-unquote.html">Quote and unquote</a> guide.</p> <h2 id="macros-in-elixir">Macros in Elixir</h2> <p>One thing to remember about macros in Elixir is that they receive AST as arguments and return AST.</p> <p>You can define a macro with <code class="language-plaintext highlighter-rouge">defmacro</code>:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyIf</span> <span class="k">do</span>
  <span class="k">defmacro</span> <span class="k">if</span><span class="p">(</span><span class="n">condition</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="n">action</span><span class="p">)</span> <span class="k">do</span>
    <span class="kn">quote</span> <span class="k">do</span>
      <span class="k">case</span> <span class="kn">unquote</span><span class="p">(</span><span class="n">condition</span><span class="p">)</span> <span class="k">do</span>
        <span class="n">x</span> <span class="ow">when</span> <span class="n">x</span> <span class="ow">in</span> <span class="p">[</span><span class="no">false</span><span class="p">,</span> <span class="no">nil</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="no">nil</span>
        <span class="n">_</span> <span class="o">-&gt;</span> <span class="kn">unquote</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div> <p>Then you can use it by:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">require</span> <span class="no">MyIf</span>

<span class="no">MyIf</span><span class="o">.</span><span class="k">if</span> <span class="no">true</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="s2">"Hello world!"</span><span class="p">)</span>

<span class="c1"># or</span>

<span class="no">MyIf</span><span class="o">.</span><span class="k">if</span> <span class="no">false</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="s2">"Will not print anything"</span><span class="p">)</span>
</code></pre></div></div> <h2 id="mental-models-about-macros">Mental models about Macros</h2> <h3 id="tree-metaphor">Tree Metaphor</h3> <p>To understand how macros work in general, I often use the tree metaphor. The entire program is just a big tree containing data and expressions as nodes, some of which are macro usages. This is literally correct with Elixir, because the compiler do convert the program into a big abstract syntax tree in the form of a deeply nested three elements tuple.</p> <p>Defining a macro is like creating a template in AST or say a sub-tree. If there are arguments, they can be injected to the sub-tree with <code class="language-plaintext highlighter-rouge">unquote</code>.</p> <p>During macro expansion, the Elixir compiler will replace each macro usage node with the AST sub-tree returned by its macro definition with the arguments injected. Because macros can be used inside another macro, macro expansion will happen repeatedly until there are no more macros.</p> <p>If we compare Elixir macros with C macros, we can see that they both offer a way to substitute an expression with a template. The difference is that in C we define the template as text and replaced as text, whereas in Elixir, the template is defined as a piece of AST and the substitution happens at the AST level as well.</p> <h3 id="copy-paste-metaphor">Copy-Paste Metaphor</h3> <p>To figure out what a particular macro does, I usually think of it as copying what the macro definition has and pasting it where the macro is being used. For unquoted arguments, mentally replace them with the corresponding values passed in.</p> <p>When the macro is more complex, this might not work, but it should still set one on the right path in understanding the macro.</p> <h2 id="summary">Summary</h2> <p>Macros are hard and they can be daunting even for experienced developers.</p> <p>In this short post, I tried to offer a different perspective in understanding macros in Elixir.</p> <p>It would fantastic if this makes it slightly easier for someone to learn about macros.</p> <p>If you’d like to learn more about Elixir macros, check out <a href="https://twitter.com/sasajuric">Saša Jurić</a>’s great blog post series <a href="https://www.theerlangelist.com/article/macros_1">Understanding Elixir Macros</a> and <a href="https://twitter.com/chris_mccord">Chris Mccord</a>’s book <a href="https://pragprog.com/titles/cmelixir/metaprogramming-elixir/">Metaprogramming Elixir</a>.</p>]]></content><author><name>Qing Wu</name></author><category term="elixir"/><category term="macro"/><category term="tiny-tips"/><summary type="html"><![CDATA[Some random thoughts about Elixir Macros]]></summary></entry><entry><title type="html">Setup Pi-hole on Raspberry Pi</title><link href="https://www.wiserfirst.com/blog/pi-hole-on-raspberry-pi/" rel="alternate" type="text/html" title="Setup Pi-hole on Raspberry Pi"/><published>2021-10-18T11:45:00+11:00</published><updated>2025-08-24T22:59:00+10:00</updated><id>https://www.wiserfirst.com/blog/pi-hole-on-raspberry-pi</id><content type="html" xml:base="https://www.wiserfirst.com/blog/pi-hole-on-raspberry-pi/"><![CDATA[<p>According to its official documentation,</p> <blockquote> <p>The Pi-hole® is a DNS sinkhole that protects your devices from unwanted content, without installing any client-side software.</p> </blockquote> <p><a href="https://pi-hole.net/">Pi-hole</a> is a great piece of software if you are interested in a bit more privacy and saving some bandwidth at the same time.</p> <p>As its name suggests, it can obviously be installed on a Raspberry Pi, but apart from that it actually runs on other Linux hardware as well. As long as your device can run one of the <a href="https://docs.pi-hole.net/main/prerequisites/#supported-operating-systems">Officially supported Operating Systems</a>, you should be able to run Pi-hole on it.</p> <p>While supporting other hardware definitely has its use cases, I consider Raspberry Pi to be the best choice for running Pi-hole in the home network, because of its low cost and very low power consumption.</p> <p>I happen to have a <a href="https://www.raspberrypi.com/products/raspberry-pi-zero/">Raspberry Pi Zero</a> that’s been collecting dust since I bought it a few years ago, which is perfect for running Pi-hole.</p> <h2 id="install-os-on-raspberry-pi">Install OS on Raspberry Pi</h2> <p>Obviously we need to install an operating system on Raspberry Pi before we could do anything with it.</p> <p>The recommended way of installing an operating system for Raspberry Pi is to use the <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a>. You’ll need a computer with an micro SD card reader to install an OS image on your card.</p> <div style="margin: auto; text-align: center; width: 100%; max-width: 680px"> <figure style="display: block"> <img src="/assets/images/2021-10-18/pi_imager.png" alt="Raspberry Pi Imager Screenshot"/> <figcaption style="text-align: center;"> Raspberry Pi Imager Screenshot </figcaption> </figure> </div> <p>I’m running it on macOS, but it supports Linux and Windows as well. Just make sure to download the correct version for your OS.</p> <p>Click <code class="language-plaintext highlighter-rouge">CHOOSE OS</code> to choose from a list of options. I’d recommended just go with <code class="language-plaintext highlighter-rouge">Raspberry Pi OS (32-bit)</code>, which is the first option, unless you have a specific purpose for the Pi and you know what you are doing.</p> <p>Then click <code class="language-plaintext highlighter-rouge">CHOOSE STORAGE</code> to select the mounted micro SD card. Remember that the imager will erase the card first, so you’ll lost everything on the card. Make sure you’ve got backup for anything you still need on the card.</p> <p>Lastly, click <code class="language-plaintext highlighter-rouge">WRITE</code> to install the selected OS on your card.</p> <p>For most of the OS options, the imager will download the OS image while writing it to the micro SD card. If you have a slow Internet connect, you can also download the OS image separately from <a href="https://www.raspberrypi.com/software/operating-systems/">the Raspberry Pi OS download page</a> and then choose <code class="language-plaintext highlighter-rouge">Use custom</code> for the OS option and select the downloaded <code class="language-plaintext highlighter-rouge">.img</code> file.</p> <h3 id="advanced-options-for-the-imager">Advanced options for the imager</h3> <p>There is also an “Advanced” menu in the imager, which you can open with <code class="language-plaintext highlighter-rouge">Ctrl-Shift-X</code>. This menu allows you to perform tasks like enabling SSH and setting admin password. As described in the next section, we can do those with terminal commands too.</p> <h2 id="setup-raspberry-pi">Setup Raspberry Pi</h2> <p>Since I find connecting a monitor, a keyboard and a mouse to the Raspberry Pi quite cumbersome, I’d like to setup a headless Raspberry Pi.</p> <p>In order to achieve that, there are a couple more things to do before booting the Raspberry Pi: enable SSH access and allow auto WiFi connection.</p> <h3 id="enable-ssh-access">Enable SSH access</h3> <p>On macOS, the SD card with Raspberry Pi OS image is usually mounted on <code class="language-plaintext highlighter-rouge">/Volumes/boot</code>.</p> <p>We can enable SSH by creating an empty file named <code class="language-plaintext highlighter-rouge">ssh</code> in the root directory of the card:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /Volumes/boot
<span class="nb">touch </span>ssh
</code></pre></div></div> <h3 id="auto-wifi-connection">Auto WiFi connection</h3> <p>I’d also like the Pi to be able to connect to WiFi when it boots up.</p> <p>In order to do that, create a text file named <code class="language-plaintext highlighter-rouge">wpa_supplicant.conf</code> with the following content:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>country=&lt;your-country-code&gt;
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
network={
    ssid="&lt;your-wifi-ssid&gt;"
    psk="&lt;your-wifi-psk"
    key_mgmt=WPA-PSK
}
</code></pre></div></div> <p>Obviously, fill in the <a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166 alpha-2 country code</a> of your country, your WiFi SSID and PSK.</p> <p>Then copy the file to the root directory of the SD card:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp </span>wpa_supplicant.conf /Volumes/boot
</code></pre></div></div> <blockquote> <p>Note: if you are using an older Raspberry Pi (like my Pi Zero), it might not support 5GHz networks</p> </blockquote> <h3 id="connect-via-ssh">Connect via SSH</h3> <p>After the two steps above, we can put the SD card in the Raspberry Pi and boot it up.</p> <p>Once it’s up and running, we should be able to connect to it via SSH:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh pi@&lt;ip_for_raspberry_pi&gt;
</code></pre></div></div> <p>The default password is <code class="language-plaintext highlighter-rouge">raspberry</code>.</p> <p>Make sure to update the admin password with <code class="language-plaintext highlighter-rouge">passed</code> after logging in.</p> <h3 id="fix-locale">Fix locale</h3> <p>When you run commands on the Raspberry Pi, you may see warnings like this</p> <blockquote> <p>-bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)</p> </blockquote> <p>So you might want to fix the locale.</p> <p>If you are comfortable with <code class="language-plaintext highlighter-rouge">vi</code> or <code class="language-plaintext highlighter-rouge">nano</code>, just edit <code class="language-plaintext highlighter-rouge">/etc/locale.gen</code> and uncomment the line starting with <code class="language-plaintext highlighter-rouge">en_US.UTF-8</code>.</p> <p>Otherwise you can run the following command to do the same:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>perl <span class="nt">-pi</span> <span class="nt">-e</span> <span class="s1">'s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/g'</span> /etc/locale.gen
</code></pre></div></div> <p>Then run the following:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>locale-gen en_US.UTF-8
update-locale en_US.UTF-8
</code></pre></div></div> <p>After that, the annoying warnings should be gone.</p> <p>Check out <a href="https://www.jaredwolff.com/raspberry-pi-setting-your-locale/">Jared Wolff’s post</a> for more details on locale.</p> <h2 id="install-pi-hole">Install Pi-hole</h2> <p>Now the Raspberry Pi is in a reasonable state, we are ready to install Pi-hole.</p> <p>The most convenient way to is to use the following command:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sSL</span> https://install.pi-hole.net | bash
</code></pre></div></div> <p>The installation process is relatively straightforward. Just follow the prompts to get it done.</p> <p>If you’d like to know what the installation script actually does, check out the source code <a href="https://github.com/pi-hole/pi-hole/blob/master/automated%20install/basic-install.sh">basic-install.sh</a>.</p> <h3 id="take-advantage-of-pi-hole-in-the-network">Take advantage of Pi-hole in the network</h3> <p>After Pi-hole is successfully installed, we still need to configure the router to use Pi-hole as the DNS server, which makes sure that all devices on the network will be protected automatically by Pi-hole.</p> <p>If that’s not supported by your router or you only want certain devices to use Pi-hole, you can configure the DNS server on each device.</p> <p>You might want to check out <a href="https://discourse.pi-hole.net/t/how-do-i-configure-my-devices-to-use-pi-hole-as-their-dns-server/245">this comprehensive guide</a> on the different options to configure Pi-hole as your DNS server.</p> <h3 id="manage-blocklists">Manage blocklists</h3> <p>Pi-hole comes with a default blocklist:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
</code></pre></div></div> <p>Which is well maintained and provides good protection without breaking normal functionality in most cases. For many this might be enough, but if you have other requirements, such as blocking adult content or targeted ads, you can add custom blocklists for improved blocking capabilities.</p> <p>Check out <a href="https://avoidthehack.com/best-pihole-blocklists">The Best PiHole Blocklists (2021)</a> for some blocklist options and more importantly, what to think about when choosing a blocklist.</p> <p>You can add/remove blocklists under <code class="language-plaintext highlighter-rouge">Group Management -&gt; Adlists</code> in the Pi-hole admin interface. After making changes there, you’ll need to either run <code class="language-plaintext highlighter-rouge">pihole -g</code> in terminal or go to <code class="language-plaintext highlighter-rouge">Tools -&gt; Update Gravity</code> and click the <code class="language-plaintext highlighter-rouge">Update</code> button.</p> <h3 id="whitelist-youtube-history-and-etc">Whitelist Youtube history and etc</h3> <p>With the default blocklist from Pi-hole, the only thing that no longer works for me is Youtube history on iOS devices.</p> <p>Adding a domain to the whitelist would solve that:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pihole <span class="nt">-w</span> s.youtube.com
</code></pre></div></div> <p>Or if you prefer the admin interface, you can add a domain in <code class="language-plaintext highlighter-rouge">Whitelist</code>.</p> <p>If you find other sites or services not working properly after introducing Pi-hole, you might want to check out the <a href="https://discourse.pi-hole.net/t/commonly-whitelisted-domains/212">Commonly Whitelisted Domains</a> and potentially whitelist relevant domains.</p> <h2 id="keep-software-up-to-date">Keep software up-to-date</h2> <p>Last but not least, keeping things up-to-date on the Raspberry Pi would be a good idea.</p> <p>Since Raspberry Pi OS is based on Debian Linux, we can use <code class="language-plaintext highlighter-rouge">apt</code> for that:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt full-upgrade
</code></pre></div></div> <p>Also we can remove packages that are no longer required with:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt autoremove
</code></pre></div></div> <h2 id="summary">Summary</h2> <p>In this post, I walked through how to install Raspberry Pi OS, setup headless Raspberry Pi, install Pi-hole, manage blocklists and etc.</p> <p>As many of my other posts, the main purpose is to serve as a reference for my future self. But if someone else finds it helpful too, I’d be very glad :slightly_smiling_face:</p>]]></content><author><name>Qing Wu</name></author><category term="adblock"/><category term="debian"/><category term="pi-hole"/><category term="raspberry-pi"/><summary type="html"><![CDATA[Install in one place and protect your entire network]]></summary></entry></feed>