<?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-11T20:37:24+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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></author><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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></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><entry><title type="html">Journey of upgrading an app to Phoenix 1.6</title><link href="https://www.wiserfirst.com/blog/upgrade-to-phoenix-16/" rel="alternate" type="text/html" title="Journey of upgrading an app to Phoenix 1.6"/><published>2021-09-19T12:25:00+10:00</published><updated>2021-09-28T12:05:00+10:00</updated><id>https://www.wiserfirst.com/blog/upgrade-to-phoenix-16</id><content type="html" xml:base="https://www.wiserfirst.com/blog/upgrade-to-phoenix-16/"><![CDATA[<p>Phoenix 1.6 RC0 was released just over three weeks ago and I’m really excited about it. Because it provides the option to replace webpack with esbuild, so that we no longer need Node for asset building.</p> <p>True that it’s just the first release candidate, but we are still one step closer to the formal release.</p> <blockquote> <p><strong>Update (28 September 2021):</strong> <a href="https://github.com/phoenixframework/phoenix/releases/tag/v1.6.0">Phoenix 1.6.0</a> was released three days ago on 25 September, so now we do have our formal release</p> </blockquote> <p>Last week, I managed to upgrade my little side project <a href="https://github.com/wiserfirst/rubiks_cube_algs_trainer">Rubik’s Cube Algorithms Trainer</a> to Phoenix 1.6 and I’ll share my journey in this post. For the most part, I was following the <a href="https://gist.github.com/chrismccord/2ab350f154235ad4a4d0f4de6decba7b">Phoenix 1.5.x to 1.6 upgrade instructions</a> by <a href="https://twitter.com/chris_mccord">Chris McCord</a>.</p> <p>If you prefer watching a video than reading, I also did a talk about it at <a href="https://www.meetup.com/elixir-sydney/events/gztkjsyccmbtb/">Elixir Sydney September 2021 meetup</a> and the recording is on <a href="https://www.youtube.com/watch?v=NR_Jk3yUEqc">Youtube</a>.</p> <h2 id="update-dependencies-in-mixfile">Update dependencies in Mixfile</h2> <p>First we need to update the dependencies in the <code class="language-plaintext highlighter-rouge">mix.exs</code> file:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">deps</span> <span class="k">do</span>
  <span class="p">[</span>
    <span class="p">{</span><span class="ss">:phoenix</span><span class="p">,</span> <span class="s2">"~&gt; 1.6.0"</span><span class="p">},</span>
    <span class="p">{</span><span class="ss">:phoenix_html</span><span class="p">,</span> <span class="s2">"~&gt; 3.0"</span><span class="p">},</span>
    <span class="p">{</span><span class="ss">:phoenix_live_view</span><span class="p">,</span> <span class="s2">"~&gt; 0.16.0"</span><span class="p">},</span>
    <span class="p">{</span><span class="ss">:phoenix_live_dashboard</span><span class="p">,</span> <span class="s2">"~&gt; 0.5"</span><span class="p">},</span>
    <span class="p">{</span><span class="ss">:telemetry_metrics</span><span class="p">,</span> <span class="s2">"~&gt; 0.6"</span><span class="p">},</span>
    <span class="p">{</span><span class="ss">:telemetry_poller</span><span class="p">,</span> <span class="s2">"~&gt; 0.5"</span><span class="p">},</span>
    <span class="o">...</span>
  <span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div> <p>Then run <code class="language-plaintext highlighter-rouge">mix deps.get</code> to install the new dependencies.</p> <p>Two thing to note here:</p> <ul> <li><del>For <code class="language-plaintext highlighter-rouge">phoenix</code>, the <code class="language-plaintext highlighter-rouge">override: true</code> option is important, because Phoenix 1.6 is still in RC</del> (This is no longer the case, since Phoenix 1.6.0 has been released)</li> <li>If you want to use the new <code class="language-plaintext highlighter-rouge">HEEx</code> templates, add <code class="language-plaintext highlighter-rouge">phoenix_live_view</code> even if you don’t actually use live view</li> </ul> <h2 id="esbuild-for-javascript-and-css-bundling-optional">esbuild for Javascript and CSS bundling (optional)</h2> <p>Next step is to use esbuild for Javascript and CSS bundling. This is an optional step in upgrading to Phoenix 1.6, but it is what I’m all excited about, so it’s not optional for me :smirk:</p> <h3 id="phoenix-asset-pipeline-overview">Phoenix asset pipeline overview</h3> <p>Before jumping into replacing webpack with esbuild, it’s worth having a quick review of the existing Phoenix asset pipeline:</p> <ul> <li>All static assets are served from <code class="language-plaintext highlighter-rouge">priv/static</code></li> <li>Javascript: webpack bundles from <code class="language-plaintext highlighter-rouge">assets/js</code> to <code class="language-plaintext highlighter-rouge">priv/static/js</code></li> <li>CSS: webpack bundles from <code class="language-plaintext highlighter-rouge">assets/css</code> to <code class="language-plaintext highlighter-rouge">priv/static/css</code></li> <li>Images and other assets: webpack copies from <code class="language-plaintext highlighter-rouge">assets/static</code> -&gt; <code class="language-plaintext highlighter-rouge">priv/static</code></li> </ul> <p>Now with the new asset pipeline based on esbuild, all static assets are still served from <code class="language-plaintext highlighter-rouge">priv/static</code> directory, so the first item stays the same.</p> <p>With webpack gone, the other three obviously will change. For JS and CSS, esbuild will handle them; but we do need to deal with images and other assets separately.</p> <p>Alright, let’s dive into how to make those changes.</p> <h3 id="remove-webpack-config-and-related-node-files">Remove webpack config and related node files</h3> <p>First we need to remove webpack config and related node files:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cd </span>assets
<span class="nv">$ </span><span class="nb">rm </span>webpack.config.js package.json
     package-lock.json .babelrc
<span class="nv">$ </span><span class="nb">rm</span> <span class="nt">-rf</span> node_modules
</code></pre></div></div> <p>If you use yarn, remove <code class="language-plaintext highlighter-rouge">yarn.lock</code> instead of <code class="language-plaintext highlighter-rouge">package-lock.json</code>.</p> <h3 id="add-esbuild-in-deps">Add esbuild in deps</h3> <p>Then add <code class="language-plaintext highlighter-rouge">esbuild</code> as a dependency:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">deps</span> <span class="k">do</span>
  <span class="p">[</span>
    <span class="o">...</span>
    <span class="p">{</span><span class="ss">:esbuild</span><span class="p">,</span> <span class="s2">"~&gt; 0.2"</span><span class="p">,</span> <span class="ss">runtime:</span> <span class="no">Mix</span><span class="o">.</span><span class="n">env</span><span class="p">()</span> <span class="o">==</span> <span class="ss">:dev</span><span class="p">},</span>
  <span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div> <h3 id="configure-esbuild">Configure esbuild</h3> <p>Next add configuration for esbuild in <code class="language-plaintext highlighter-rouge">config/config.exs</code>:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/config.exs</span>
<span class="n">config</span> <span class="ss">:esbuild</span><span class="p">,</span>
  <span class="ss">version:</span> <span class="s2">"0.12.18"</span><span class="p">,</span>
  <span class="ss">default:</span> <span class="p">[</span>
    <span class="ss">args:</span> <span class="sx">~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets)</span><span class="p">,</span>
    <span class="ss">cd:</span> <span class="no">Path</span><span class="o">.</span><span class="n">expand</span><span class="p">(</span><span class="s2">"../assets"</span><span class="p">,</span> <span class="n">__DIR__</span><span class="p">),</span>
    <span class="ss">env:</span> <span class="p">%{</span><span class="s2">"NODE_PATH"</span> <span class="o">=&gt;</span> <span class="no">Path</span><span class="o">.</span><span class="n">expand</span><span class="p">(</span><span class="s2">"../deps"</span><span class="p">,</span> <span class="n">__DIR__</span><span class="p">)}</span>
  <span class="p">]</span>
</code></pre></div></div> <blockquote> <p>Note: here we are providing the relative path from <code class="language-plaintext highlighter-rouge">config</code> to <code class="language-plaintext highlighter-rouge">assets</code> with the <code class="language-plaintext highlighter-rouge">:cd</code> option, so that the esbuild command can be run in the <code class="language-plaintext highlighter-rouge">assets</code> directory. Given that, if you have a umbrella app, the path should be something like <code class="language-plaintext highlighter-rouge">../apps/your_web_app/assets</code>.</p> </blockquote> <h3 id="update-watcher-in-endpoint-configuration">Update watcher in endpoint configuration</h3> <p>In your <code class="language-plaintext highlighter-rouge">config/dev.exs</code> file, there should be a node watcher that uses webpack under the endpoint configuration. We want to replace that one with the esbuild watcher below:</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">:your_web_app</span><span class="p">,</span> <span class="no">YourWebApp</span><span class="o">.</span><span class="no">Endpoint</span><span class="p">,</span>
  <span class="o">...</span><span class="p">,</span>
  <span class="ss">watchers:</span> <span class="p">[</span>
    <span class="ss">esbuild:</span> <span class="p">{</span><span class="no">Esbuild</span><span class="p">,</span> <span class="ss">:install_and_run</span><span class="p">,</span> <span class="p">[</span><span class="ss">:default</span><span class="p">,</span> <span class="sx">~w(--sourcemap=inline --watch)</span><span class="p">]}</span>
  <span class="p">]</span>
</code></pre></div></div> <blockquote> <p>Note: this is for local development only.</p> </blockquote> <h3 id="move-images-and-other-assets">Move images and other assets</h3> <p>Then we deal with images.</p> <p>The following is not mentioned in the Phoenix 1.6 upgrade instructions, but I found <a href="https://www.reddit.com/r/elixir/comments/p9t68v/with_the_new_esbuild_transition_what_do_you_use/ha0cskv?context=3">José Valim’s recommendation</a> in a reddit thread.</p> <p>He recommends to move everything in <code class="language-plaintext highlighter-rouge">assets/static</code> to <code class="language-plaintext highlighter-rouge">priv/static</code>; stop ignoring <code class="language-plaintext highlighter-rouge">priv/static</code> and commit it in version control; also ignore <code class="language-plaintext highlighter-rouge">priv/static/assets</code> instead, since that’s where esbuild puts the compiled Javascript and CSS files.</p> <h3 id="add-assetsdeploy-mix-alias">Add <code class="language-plaintext highlighter-rouge">assets.deploy</code> mix alias</h3> <p>Then we add a new mix alias for deployment:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defp</span> <span class="n">aliases</span> <span class="k">do</span>
  <span class="p">[</span>
    <span class="o">...</span><span class="p">,</span>
    <span class="s2">"assets.deploy"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"esbuild default --minify"</span><span class="p">,</span> <span class="s2">"phx.digest"</span><span class="p">]</span>
  <span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div> <p>As we can see, it has two parts:</p> <ol> <li>esbuild will create minified Javascript and CSS and put them in <code class="language-plaintext highlighter-rouge">priv/static/assets</code></li> <li>the <code class="language-plaintext highlighter-rouge">phx.digest</code> task will add digests for all static assets <code class="language-plaintext highlighter-rouge">priv/static</code></li> </ol> <p>This is for deployment, so we only need to run it on build servers. For example when deploying to Heroku or the like, make sure it is run.</p> <p>If you did run it locally, it would generate a whole bunch of digested assets. Since we no longer ignore <code class="language-plaintext highlighter-rouge">priv/static</code>, they would show up when you run <code class="language-plaintext highlighter-rouge">git status</code> for example and could be annoying.</p> <p>We can remove them with the following command:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mix phx.digest.clean <span class="nt">--all</span>
</code></pre></div></div> <h3 id="update-layouts">Update layouts</h3> <p>As mentioned in the last section, esbuild puts compiled Javascript and CSS in <code class="language-plaintext highlighter-rouge">priv/static/assets</code> directory, so we need to update the references to them in the layouts, usually in <code class="language-plaintext highlighter-rouge">app.html.eex</code> or <code class="language-plaintext highlighter-rouge">root.html.eex</code>:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># update</span>
<span class="no">Routes</span><span class="o">.</span><span class="n">static_path</span><span class="p">(</span><span class="nv">@conn</span><span class="p">,</span> <span class="s2">"/js/app.js"</span><span class="p">)</span>
<span class="no">Routes</span><span class="o">.</span><span class="n">static_path</span><span class="p">(</span><span class="nv">@conn</span><span class="p">,</span> <span class="s2">"/css/app.css"</span><span class="p">)</span>
<span class="c1"># to</span>
<span class="no">Routes</span><span class="o">.</span><span class="n">static_path</span><span class="p">(</span><span class="nv">@conn</span><span class="p">,</span> <span class="s2">"/assets/app.js"</span><span class="p">)</span>
<span class="no">Routes</span><span class="o">.</span><span class="n">static_path</span><span class="p">(</span><span class="nv">@conn</span><span class="p">,</span> <span class="s2">"/assets/app.css"</span><span class="p">)</span>
</code></pre></div></div> <h3 id="update-plugstatic-configuration">Update <code class="language-plaintext highlighter-rouge">Plug.Static</code> configuration</h3> <p>Last step for the new asset pipeline is to update configuration for <code class="language-plaintext highlighter-rouge">Plug.Static</code>.</p> <p>We need to add the new <code class="language-plaintext highlighter-rouge">assets</code> directory in the <code class="language-plaintext highlighter-rouge">:only</code> option and also remove <code class="language-plaintext highlighter-rouge">js</code> and <code class="language-plaintext highlighter-rouge">css</code> from there since we no longer have them.</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plug</span> <span class="no">Plug</span><span class="o">.</span><span class="no">Static</span><span class="p">,</span>
  <span class="ss">at:</span> <span class="s2">"/"</span><span class="p">,</span>
  <span class="ss">from:</span> <span class="ss">:my_app</span><span class="p">,</span>
  <span class="ss">gzip:</span> <span class="no">false</span><span class="p">,</span>
  <span class="ss">only:</span> <span class="sx">~w(assets fonts images favicon.ico robots.txt)</span>
</code></pre></div></div> <p>With the changes above, the new asset pipeline should be working, which means we are officially free of webpack and node in our Phoenix application :tada:</p> <h2 id="migrate-to-heex-templates-optional">Migrate to <code class="language-plaintext highlighter-rouge">HEEx</code> templates (optional)</h2> <p>With Phoenix 1.6 the leex templates are deprecated and there is a new <code class="language-plaintext highlighter-rouge">HEEx</code> engine, which is being used in all the HTML files generated by <code class="language-plaintext highlighter-rouge">phx.new</code>, <code class="language-plaintext highlighter-rouge">phx.gen.html</code> etc.</p> <p>It is HTML-aware and it enforces valid markup. It’s also more strict for the Elixir expressions inside tags.</p> <p>In order to use it in an existing Phoenix project, make sure <code class="language-plaintext highlighter-rouge">phoenix_live_view</code> is added as a dependency, because the <code class="language-plaintext highlighter-rouge">HEEx</code> engine is part of <code class="language-plaintext highlighter-rouge">phoenix_live_view</code>.</p> <p>Then rename all the existing <code class="language-plaintext highlighter-rouge">.html.eex</code> and <code class="language-plaintext highlighter-rouge">.html.leex</code> templates to <code class="language-plaintext highlighter-rouge">.html.heex</code></p> <h3 id="update-elixir-expressions">Update Elixir expressions</h3> <p>When Elixir expressions appear in the body of HTML tags, <code class="language-plaintext highlighter-rouge">HEEx</code> templates use <code class="language-plaintext highlighter-rouge">&lt;%= ... %&gt;</code> for interpolation just like <code class="language-plaintext highlighter-rouge">EEx</code> templates.</p> <p>So code in the following example stays the same:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="n">h2</span><span class="o">&gt;</span><span class="no">Hello</span> <span class="o">&lt;%=</span> <span class="nv">@name</span> <span class="p">%</span><span class="err">&gt;</span><span class="o">&lt;/</span><span class="n">h2</span><span class="o">&gt;</span>

<span class="o">&lt;%=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">names</span><span class="p">,</span> <span class="k">fn</span> <span class="n">name</span> <span class="o">-&gt;</span> <span class="p">%</span><span class="o">&gt;</span>
  <span class="o">&lt;</span><span class="n">li</span><span class="err">&gt;</span><span class="o">&lt;%=</span> <span class="n">name</span> <span class="p">%</span><span class="err">&gt;</span><span class="o">&lt;/</span><span class="n">li</span><span class="o">&gt;</span>
<span class="o">&lt;</span><span class="p">%</span> <span class="k">end</span><span class="p">)</span> <span class="p">%</span><span class="o">&gt;</span>
</code></pre></div></div> <p>But when an Elixir expression is used inside a tag, as the attribute value for example:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="n">div</span> <span class="n">id</span><span class="o">=</span><span class="s2">"&lt;%= @id %&gt;"</span><span class="o">&gt;</span>
</code></pre></div></div> <p>It needs to be updated to:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="n">div</span> <span class="n">id</span><span class="o">=</span><span class="p">{</span><span class="nv">@id</span><span class="p">}</span><span class="o">&gt;</span>
</code></pre></div></div> <p>Notice the <code class="language-plaintext highlighter-rouge">EEx</code> interpolation is inside double quotes, the curly braces are not. So the Elixir expression has to serve as the whole attribute value, not part of it.</p> <p>With <code class="language-plaintext highlighter-rouge">EEx</code>, the Elixir expression can serve as part of the attribute value:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="n">a</span> <span class="n">href</span><span class="o">=</span><span class="s2">"/prefix/&lt;%= @item.text %&gt;"</span><span class="o">&gt;</span>
</code></pre></div></div> <p>In situations like this, directly replacing it with a pair of curly braces won’t work.</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># this doesn't work</span>
<span class="o">&lt;</span><span class="n">a</span> <span class="n">href</span><span class="o">=</span><span class="s2">"/prefix/{@item.text}"</span><span class="o">&gt;</span>
</code></pre></div></div> <p>One way I could think of is to interpolate the original Elixir term in a string and then put that string in a pair of curly braces like the following:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="n">a</span> <span class="n">href</span><span class="o">=</span><span class="p">{</span><span class="s2">"/prefix/</span><span class="si">#{</span><span class="nv">@item</span><span class="o">.</span><span class="n">text</span><span class="si">}</span><span class="s2">"</span><span class="p">}</span><span class="o">&gt;</span>
</code></pre></div></div> <p>Now the resulting string is the Elixir expression and it serves as the whole attribute value, so this works.</p> <p>After making those changes, the new <code class="language-plaintext highlighter-rouge">HEEx</code> templates should be working.</p> <h2 id="deployment">Deployment</h2> <p>There are changes to the deployment process as well and I’ll cover that for Gigalixir, since that’s where my app is deployed to. But if you use Heroku, the changes should be fairly similar.</p> <p>Since we no longer use webpack and node to build assets, <code class="language-plaintext highlighter-rouge">phoenix_static_buildpack</code> is not necessary any more. We can just get rid of it.</p> <p>Also we need to make sure the <code class="language-plaintext highlighter-rouge">assets.deploy</code> task is run during deployment. We can use <code class="language-plaintext highlighter-rouge">hook_post_compile</code> in Elixir buildpack for that.</p> <p>Just add this line in your <code class="language-plaintext highlighter-rouge">elixir_buildpack.config</code>:</p> <div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">hook_post_compile</span><span class="o">=</span><span class="s2">"eval mix assets.deploy &amp;&amp; rm -f _build/esbuild"</span>
</code></pre></div></div> <p>With those two changes, my app can be deployed to Gigalixir without any issue.</p> <p>If you’d like to deploy a new Phoenix 1.6 app to Gigalixir, check out the <a href="https://hexdocs.pm/phoenix/1.6.0-rc.0/gigalixir.html">full guide</a>.</p> <h2 id="new-generators">New generators</h2> <p>I’d like to quickly mention that Phoenix 1.6 also ships with two new generators:</p> <ul> <li><code class="language-plaintext highlighter-rouge">phx.gen.auth</code> generates a complete authentication solution for your application</li> <li><code class="language-plaintext highlighter-rouge">phx.gen.notifier</code> generates a notifier for sending emails</li> </ul> <p>In my little app, I didn’t need to use them. But if you do need an authentication solution or a notifier, check them out.</p> <h2 id="summary">Summary</h2> <p>The release of Phoenix 1.6 is quite exciting, because by default webpack and node is replaced by esbuild. Now we can focus on developing the application in Elixir and Phoenix, without wasting time on breaking changes and nonsense security alerts in node dependencies.</p> <p>In this post, I walked through how I upgraded my humble little Phoenix app to 1.6, with a focus on updating the asset pipeline to use esbuild and migrating to <code class="language-plaintext highlighter-rouge">HEEx</code> templates. Hope this helps someone who’s trying to do the same.</p> <p>Phoenix 1.6 do have other new features that are not covered in this post. For those, make sure to check out the <a href="https://www.phoenixframework.org/">release note</a>.</p>]]></content><author><name>Qing Wu</name><email>wiserfirst@gmail.com</email><uri>https://www.wiserfirst.com</uri></author><category term="elixir"/><category term="esbuild"/><category term="phoenix"/><category term="webpack"/><summary type="html"><![CDATA[Say goodbye to webpack and Node in your Phoenix applications]]></summary></entry></feed>