<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/feed.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Will Richardson</title>
    <description>Computers, photography, and related matters.</description>
    <link>https://willhbr.net/</link>
    <atom:link href="https://willhbr.net/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Sat, 14 Mar 2026 00:59:24 +1100</pubDate>
    <lastBuildDate>Sat, 14 Mar 2026 00:59:24 +1100</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>Language Servers in Containers</title>
        <pubDate>Fri, 13 Mar 2026 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2026/03/13/language-servers-in-containers/</link>
        <guid isPermaLink="true">https://willhbr.net/2026/03/13/language-servers-in-containers/</guid>
        <description><![CDATA[<p>The purpose of <a href="https://codeberg.org/willhbr/pod">my container-management tool <code>pod</code></a> is to make it easier to do development and deployment using containers, specifically with Podman. Read more about <a href="/2023/06/08/overcoming-a-fear-of-containerisation/">why I wrote it</a> and <a href="/2023/06/08/pod-the-container-manager/">what it does</a>.</p>

<p>Recently I’ve been trying out the <a href="https://helix-editor.com">Helix editor</a> in place of Vim (more on that once I’ve been using it some more). The standout feature of Helix for me is first-class <a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol</a> (LSP) support. This isn’t something I’d set up in Vim. I have <a href="https://github.com/vim-syntastic/syntastic">Syntastic</a> installed and it’ll occasionally show me errors.</p>

<p>I wanted to write Swift using <a href="https://github.com/swiftlang/sourcekit-lsp">SourceKit LSP</a> in Helix, but sadly Swift only supports up to Ubuntu 24.04 and I’m currently running 25.10. Trying to run the LSP on my newer version will fail as it <a href="https://forums.swift.org/t/ubuntu-25-10-and-libxml2/83239/4">can’t find <code>libxml2</code></a>. I could run a VM, run my whole editor in a container, or even downgrade my whole machine to the old Ubuntu version, but there’s a better way.</p>

<p>LSP works by the editor running a command (like <code>rust-analyzer</code>) which exposes a <a href="https://www.jsonrpc.org/specification">JSON RPC</a> connection using standard input and output to read and write requests and responses. This is great when the LSP command is installed directly on your system, especially because the default Helix config will point it to the right executable without you having to do anything.</p>

<p>Since the LSP is just running an arbitrary command and looking at the input/output streams, we can just change that command and run the LSP in a container ourselves. The editor doesn’t have to know anything about containers. As long as both processes have access to the same files, they’ll be happy.</p>

<p>This was a little bit tricky to get working. If the SourceKit LSP gets input it’s not expecting it will crash, and if Helix gets back a weird response it’ll just do nothing. Thankfully you can debug this a little with the <code>:log-open</code> command in Helix, which will show standard error for the LSP command.</p>

<p>Basically we can set the LSP to run something like this:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ podman run \
  --workdir=/src \
  --interactive \
  --mount=type=bind,src=.,dst=/src \
  '--entrypoint=["sourcekit-lsp"]' \
  docker.io/library/swift:latest
</code></pre></div></div>

<p>The issue with this is that the editor and the LSP have to agree on the file paths.<sup id="fnref:remapping"><a href="#fn:remapping" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> The editor will say “I’m opening <code>/home/will/Projects/some_file.swift</code>” and then since the container has the code in <code>/src</code> and has no idea what <code>/home/will/Projects</code> is, it’ll just fail to do anything. Eventually I got this working by setting the <code>--workdir</code> to match the current directory path, but that’s finicky.</p>

<p>In order to make this as easy as possible, I’ve <a href="https://codeberg.org/willhbr/pod/commit/efabf9d4eaa9441327a2f8465a8112e5dd5e5c5e">added a new <code>pod lsp</code> subcommand</a> that will manage running the server in a container. It looks at the configured bind mounts and rewrites the requests and responses so the host and container get the paths they’re expecting.</p>

<p>The LSP can be configured in the <code>pods.yaml</code> config file, either as a standard container:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults:
  lsp: lsp-container

containers:
  lsp-container:
    image: docker.io/library/swift:latest
    bind_mounts:
      .: /src
    entrypoint: [sourcekit-lsp]
</code></pre></div></div>

<p>This allows for multiple LSPs in the same project, which might be handy. However since this is a tool for me and I’m usually using one language at a time, I added a shorthand LSP config. This will add bind mounts and set the entry point, similar to how development containers work.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lsp:
  image: docker.io/library/swift:latest
  command: [sourcekit-lsp]
</code></pre></div></div>

<p>This works for actions and jumping to definitions within your own code, but when you jump to the definition of a standard library type, SourceKit will write a temporary file for the editor to display. That file lives in <code>/tmp/sourcekit-lsp</code> inside the container, which the editor doesn’t have access to. Of course you can add the appropriate <code>bind_mount</code> config, but instead I’ve made a shorthand <code>expose_paths</code> field that will translate into temporary directories on the host that get bound to the paths in the container.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lsp:
  image: docker.io/library/swift:latest
  command: [sourcekit-lsp]
  expose_paths:
    - /tmp/sourcekit-lsp
</code></pre></div></div>

<p>Since the config for the LSP lives in <code>pods.yaml</code>, you just need one Helix config to point a certain language at Pod instead of a local command:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[language-server.pod]
command = "pod"
args = ["lsp"]

[[language]]
name = "swift"
scope = "source.swift"
file-types = ["swift"]
roots = ["Package.swift"]
language-servers = ["pod"]
</code></pre></div></div>

<p>I’ve only just added this, so no doubt there will be changes to the config as I use it more and find the sharp edges. You can <a href="https://codeberg.org/willhbr/pod/commit/efabf9d4eaa9441327a2f8465a8112e5dd5e5c5e">see the code for the feature</a> or <a href="https://codeberg.org/willhbr/pod">read more about pod on Codeberg</a>.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:remapping">
      <p>Some editors support config to do this remapping themselves, but Helix does not. <a href="#fnref:remapping" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2026/03/13/language-servers-in-containers/">Permalink</a>
&bull; March 13, 2026
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Language%20Servers%20in%20Containers">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Async Programming Is Just @Inject Time</title>
        <pubDate>Mon, 02 Mar 2026 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2026/03/02/async-inject-and-effects/</link>
        <guid isPermaLink="true">https://willhbr.net/2026/03/02/async-inject-and-effects/</guid>
        <description><![CDATA[<p>All I really wanted to do was learn a little bit more about different models for error handling, but then I kept seeing “effects” and “effect systems” mentioned, and after reading about <a href="https://koka-lang.github.io/">Koka</a> and <a href="https://effekt-lang.org/">Effekt</a> I think I’ve been converted. I want effects now. So here’s what I wished I could have read a few weeks ago.</p>

<p>To start with, you need to remember that functions don’t exist. They’re made up. They’re a social construct.</p>

<p>Your CPU doesn’t know or care what functions are,<sup id="fnref:have-not-asked"><a href="#fn:have-not-asked" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> they’re purely a book-keeping abstraction that makes it easier for you to reason about your code, and for the compiler to give you some useful feedback about your code. It’s the whole idea with <a href="https://en.wikipedia.org/wiki/Structured_programming">structured programming</a>: build some abstractions and have a compiler that can make guarantees about them.</p>

<p>I’ve never really done much assembly so this wasn’t something I’d had to contend with too much, but functions are interesting because they’re a fixed entry point with a dynamic return point. Let me show you what I mean with this C program:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int first_function() {
  // ...
  return 10;
}

int some_function() {
  // ...
  int number = first_function();
  return 4 + number;
}

void main() {
  first_function();
  some_function();
}
</code></pre></div></div>

<p>When this program is compiled, the compiler knows exactly where the instruction pointer needs to jump to get to <code>first_function</code> and <code>some_function</code>, since it knows exactly where in the executable it put them. Chances are that if you looked at the assembly they would each just be a single instruction to jump a nice fixed offset.</p>

<p>What happens when we get to the <code>return</code> statements? <code>first_function</code> is called from both <code>some_function</code> and <code>main</code>—there isn’t just a single place that we can jump back to. The compiler doesn’t know when it’s generating the code for <code>first_function</code> who’s going to be calling it.</p>

<p>How this works is that alongside any function arguments, there’s an invisible argument<sup id="fnref:architecture"><a href="#fn:architecture" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> passed that contains the position of the instruction where it made the jump to the top of the function. The compiler knows what the instruction address is—it’s the one that puts it there—and so for each function call site, that’s just a static piece of information that gets passed in. At the end of each function, the compiler just has to generate some code to read that argument (usually stored in a CPU register somewhere, but it doesn’t have to be), jump back to that location, and continue execution.</p>

<p>You don’t think about this complexity because the abstraction is so solid and yet gives immense flexibility to write complicated programs.</p>

<p>The resolution of which function to call can get more complicated by taking into account the number of arguments and their types, instead of just the name of the function.</p>

<p>That’s the simplest case—static dispatch that is known at compile time—but higher-level languages introduce dynamic dispatch, where a function call could end up jumping to one of many different locations. A great example of this is Java:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class MyClass {
  @Override
  public String toString() {
    return "my class";
  }
}

Object someObject = new MyClass();
someObject.toString();
</code></pre></div></div>

<p>The <code>toString</code> method that gets called depends on the type of the receiver object. This isn’t determined at compile time, but instead a lookup that happens at runtime. The compiler effectively generates a <code>switch</code> statement that looks at the result of <code>getClass</code> and <em>then</em> calls the right method. It’s smarter than that for performance I’m sure, but conceptually that’s what it’s doing.</p>

<p>This abstraction continues to work really well because if you’ve developed in Java (or any of the many many languages that share this behaviour) you quickly internalise the behaviour of the method resolution algorithm, and it’s almost never surprising which bit of code ends up being executed. The compiler might need a runtime lookup to check, but you can use your big human brain and work it out with deduction while you write the code.</p>

<p>So in Java (and basically every other object-oriented language) we have dynamic function dispatch as well as a dynamic return jump at the end of each function. We can pass an object to a function, and call a method on that object. Since the receiving function doesn’t know the type of the object at compile time, any method calls on it will be completely dynamic:<sup id="fnref:or-c"><a href="#fn:or-c" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>String someMethod(Object object) {
  return "This could be anything: " + object.toString();
}
</code></pre></div></div>

<p><code>someMethod</code> might be statically dispatched, but the call to <code>toString</code> will have to be dynamically resolved depending on the type of <code>object</code>.</p>

<p>In <code>someMethod</code>, the call to <code>toString</code> will end up jumping to code that is entirely controlled by the object that is passed in as an argument. The CPU (or in this case, JVM) will lookup the location of <code>toString</code> on whatever type of object it is, and jump there.</p>

<p>Just like with the function resolution algorithm, this complexity is manageable both because of the function call abstraction—we know that control will jump into the other function and then return back to our function—as well as type safety—we know the returned type will be a <code>String</code>, so we don’t need to worry about how we got it.</p>

<p>This is something that I find interesting in Rust; since there’s no runtime dynamic dispatch “by default” you have to be very explicit by wrapping your type in <code>Box&lt;dyn MyTrait&gt;</code>, or if you want your dynamism at compile time you can use <code>impl MyTrait</code>.</p>

<p>Now if we’re going to jump to an arbitrary bit of code, why not put that bit of code at the call site? That’s what happens when we create an anonymous subclass:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>someMethod(new Object() {
  @Override
  public String toString() {
    return "heh a new string";
  }
});
</code></pre></div></div>

<p>The actual location in the source file doesn’t really matter—the compiler will end up putting it wherever it feels like—but from a syntax point of view, we’ve now got control flow that jumps into <code>someMethod</code>, then back into our <code>toString</code> method, returns to <code>someMethod</code>, and then finally back to the call site.</p>

<p>This is such a useful pattern that most languages have dedicated syntax for this: closures! I love closures so much that <a href="/2024/06/28/a-critique-of-closure-syntaxes/">I wrote a review of the various closure syntaxes</a>. Let’s jump out of the JVM for now and appreciate this lovely Swift closure:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[1, 2, 3].map { number in
  number * 3
}
</code></pre></div></div>

<p>Instead of all that boilerplate to make a new object, we basically just write a block of code that will be used by the function we’re calling. What’s interesting here is that we’re not in complete control when that block of code is running. It might appear like it, but we can’t do anything except give a value back to the function.</p>

<p>This creates the limitation where you can’t create custom control flow that integrates with control flow that’s built into the language. Closures can provide values and have side effects, but they’ve got limited ability to stop the function that called them from running.</p>

<p>Both Ruby and Crystal work around this limitation in interesting ways, but that’s getting a little bit ahead of ourselves.</p>

<p>We’re going to forget about closures for a minute and talk about error handling. I promise it’ll make sense.</p>

<h1 id="error-handling">Error Handling</h1>

<p>The most basic form of error handling is what you get in Go; if something didn’t work, you return a value that says so. By convention the caller checks that value and typically just returns it to say that whatever it was trying to do also didn’t work.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>func getConfigPath() (string, error) {
  path, set := os.LookupEnv("CONFIG_PATH")
  if !set {
    // The variable isn't set, report an error
    return "", fmt.Errorf("CONFIG_PATH not set")
  }
  return path, nil
}
</code></pre></div></div>

<p>This is conceptually very simple, it’s building slightly on the function abstraction by allowing multiple return values, but little else. If a function can fail, you can see from its function signature that it will return an error, like in <code>getConfigPath</code> above.</p>

<p>With this model we have to write out the <code>return nil, err</code>  after each function call, but semantically we can think of control flow “jumping” to the point where we do something other than immediately return the error.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>func getConfig() (*conf.Config, error) {
  path, err := getConfigPath()
  if err != nil {
    return nil, err
  }
  f, err := os.Open(path)
  if err != nil {
    return nil, err
  }
  config, err := configFromFile(f)
  if err != nil {
    return nil, err
  }
  return config, nil
}

config, err := getConfig()
if err != nil {
  panic(err)
}
</code></pre></div></div>

<p>In this example, any error in loading the path, reading the file, or parsing the config will all direct control flow back to the top-level code and to the <code>panic</code> call.</p>

<p>Skipping over macros that make it more succinct to return an error, the next iteration of this pattern is checked exceptions in Java. Any function that can fail is annotated with what is effectively a second return value. The thing that’s different is that there’s nothing at the call site needed to return this value, it will be implicitly passed back up through the call stack (each one dynamically resolved, remember) until we hit a <code>catch</code> block, which is just a bit of code that takes that return value and does something with it, not that different to the Go example above.</p>

<p>If we ignore the fact that exceptions in Java are typed, all that’s actually happening here is that every time we enter a <code>try</code> block, the compiler records in memory the location of the instruction corresponding to the start of the <code>catch</code> block. As we keep calling more functions, some of them might have <code>try</code> blocks of their own, and those are added onto a stack—a shorter stack than the actual call stack, since not all functions have a <code>try/catch</code>. When an exception is thrown, instead of looking up the location the function is supposed to return to, we consult the stack to find the topmost <code>catch</code> block, and jump straight there. We’ve just done a <code>return</code> that has skipped over multiple functions all in one go.</p>

<p>Of course the actual behaviour is much more complicated as it has to worry about <code>finally</code> blocks and types and all that, but the core idea is the same.</p>

<p>Have you got all that? This is where things get weird.</p>

<p>When an exception is thrown, what if the compiler grabbed the instruction pointer and stored it somewhere before jumping out to the <code>catch</code> block? Then if you wanted, inside the <code>catch</code> you could choose to jump back—multiple layers of function calls—into the code that failed as though nothing had happened.</p>

<p>Let’s say that we could grab the current instruction pointer location—which the compiler will know for every line of code, since it’s the one generating the instructions—with a special variable called <code>__instruction__</code>.</p>

<p>Something like this (if C had <code>catch</code> … or <code>throw</code>):<sup id="fnref:ignoring-stacks"><a href="#fn:ignoring-stacks" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int some_function() {
  print("At the start...")
  throw __location__;
  print("I'm back!");
}

try {
  some_function();
} catch(error_location) {
  print("Caught an exception!");
  goto error_location;
}
print("Finished.");
</code></pre></div></div>

<p>In <code>some_function</code> we <code>throw</code> and jump out to the nearest <code>try</code> in our call stack, passing the current instruction back. In the code up the call stack we can run some code and then <code>goto</code> back to where the <code>throw</code> happened, resuming the function where we left off.</p>

<p>The output would look like this:</p>

<pre><code>At the start...
Caught an exception!
I'm back!
Finished.
</code></pre>

<p>Well, that’s effects. Almost.</p>

<h1 id="coroutines">Coroutines</h1>

<p>There is another feature similar to effects that is called “coroutines”. This is confusing because that’s what people often call lightweight threads, which are often implemented with some version of coroutines, even if you can’t use them in the language for other stuff. Coroutines allow you to stop the execution of a function and then resume it later, usually passing values back and forth in those steps.</p>

<p>I first came across coroutines in the <a href="https://wren.io">Wren</a> programming language. I got very confused by its <a href="https://wren.io/concurrency.html">concurrency</a> since by default it’s not the “throw stuff at the wall and it’ll run at kinda the same time” model that Go and Crystal have.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>var fiber = Fiber.new {
  System.print("Before yield")
  Fiber.yield()
  System.print("Resumed")
}

System.print("Before call")
fiber.call()
System.print("Calling again")
fiber.call()
System.print("All done")
</code></pre></div></div>

<p>That gives this output:</p>

<pre><code>Before call
Before yield
Calling again
Resumed
All done
</code></pre>

<p>Instead of the fibre running in the background (or “background” depending on your scheduler) it runs until it hits <code>Fiber.yield()</code> then it stops and waits for someone to call <code>.call()</code> again.</p>

<p>This is really powerful for writing a lexer and parser that work together without having complicated code, or by storing an entire intermediate result in memory before passing it to the next stage. The lexer can trundle along and once it’s got a full token it can <code>yield()</code> that value. The parser just continually runs <code>.call()</code> whenever it needs a new token to process. They’re passing off control between each other in a more complicated way than just calling a single function and getting back a single result. The code in the lexer and parser can be more freely structured as any function can <code>yield()</code> or <code>call()</code> whenever a value is found or needed.</p>

<p>Remember how I wrote <a href="/2023/10/31/how-i-learned-to-stop-worrying-and-love-concurrency/">thousands of words about concurrent programming</a>? Well the secret to any language that has <code>async</code>/<code>await</code> is basically that they can do this “jump to <code>catch</code> and then resume again later” trick.</p>

<p>Ignore the fact that <code>catch</code> usually means exceptions which usually means some kind of failure. A piece of code is running and it just started some work that’s going to take a long time in the background, there’s no point waiting and the program can do something more useful while the stuff happens in the background. It “throws” an exception that is caught by a scheduler multiple layers of function calls up the stack. The scheduler saves the return address into a list of pending work to get back to, and then goes to find something that it can make progress on. Eventually it completes the other work and is signalled that our background task is complete. It pops the return address off the list and jumps to it, continuing the function call exactly where it left off as though nothing happened.</p>

<p>If you take nothing else from this post, just know that <code>async</code>/<code>await</code> is just weird exceptions that you can undo.</p>

<p>Now the problem that plagues both <code>async</code>/<code>await</code> and exceptions is that they’re typically not integrated into the rest of the type system. In Java you can’t have a type or function that is generic over whether it will throw an exception.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>String readFileOrFail(String path) throws IOException, FileNotFoundException {
  File file = new File(path);
  if (file.exists()) {
    return FileUtils.read(path);
  } else {
    throw new FileNotFoundException("file doesn't exist");
  }
}

List.of("one.txt", "two.txt", "three.txt")
  .stream()
  // doesn't work!
  .map(name -&gt; readFileOrFail(name))
  .collect();
</code></pre></div></div>

<p>The <code>map</code> method only accepts a lambda that doesn’t throw any checked exceptions, so we can’t directly call our <code>readFileOrFail</code> method. Ideally it would be able to generically say “I throw the same exceptions as the lambda I receive” but you can’t do that in the Java type system.</p>

<p>This isn’t helped by the fact that Java has mostly given up on checked exceptions and instead opted for purely unchecked, runtime exceptions that offer no compile-time guarantees.</p>

<p>Swift is a little better in that it has the <code>rethrows</code> keyword that can mark a closure and function as failing with the same exceptions as the closure.</p>

<p>You get the same story with <code>async</code> functions. Swift has a <a href="https://developer.apple.com/documentation/swift/asyncsequence">whole separate library</a> for dealing with async operations on collections, because the methods on the existing collections can’t be generic to support both synchronous and asynchronous versions. There’s no such thing as <code>re-async</code>.</p>

<p>So here we go: all any of these things—closures, exceptions, suspending functions—are just ways of jumping forwards and backwards to different places, and some compiler guarantees to ensure that any jumping can happen in a structured, safe way. And that’s what effects give you, and some more.</p>

<h1 id="effekt">Effekt</h1>

<p><a href="https://effekt-lang.org/">Effekt</a> is a research language with effect handlers and effect polymorphism (it says so on the website!). I also read the docs on <a href="https://koka-lang.github.io/">Koka</a> but ended up writing the most code in Effekt.</p>

<p>From the <a href="https://effekt-lang.org/tour/effects">language tour on Effekt effects</a>, an effect is written with an <code>interface</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>interface Exception {
  def throw(msg: String): Nothing
}
</code></pre></div></div>

<p>In this case we’ll <code>throw</code> with a <code>String</code> and then the effect handler will give us <code>Nothing</code> back. In this case that’s a somewhat magical <code>Nothing</code> type that tells the compiler the function will never return, but it could be a real value, which we’ll see in later examples.</p>

<p>Then we have a function that uses the effect:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def div(a: Double, b: Double) =
  if (b == 0.0) { do throw("division by zero") }
  else { a / b }
</code></pre></div></div>

<p>What’s interesting here is how that <code>throw</code> changes the function signature of <code>div</code>. In this example it’s elided since it will just be inferred by the compiler. We could write it as <code>Double / { Exception }</code>, which says we’re returning a <code>Double</code> and we’ll use the <code>Exception</code> effect. This means we can only call it from somewhere with an <code>Exception</code> effect handler, like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>try {
  div(4, 0)
} with Exception {
  def throw(msg) = {
    println("oh no the div failed: " ++ msg)
  }
}
println("finished")
</code></pre></div></div>

<p>The control flow will start at <code>try</code>, then jump to the <code>div</code> function, since the <code>b</code> argument is <code>0</code>, <code>div</code> will invoke the <code>throw</code> effect. The effect will jump control flow back down into the <code>def throw</code> block, and we’ll print the error. Since we didn’t call <code>resume()</code> the control flow will continue after the <code>try</code> block and run the last <code>println</code>.</p>

<p>Effekt effects get their power with the <code>resume</code> keyword. This swaps them from acting like exceptions and makes them act like <code>async</code>/<code>await</code>. Control flow jumps to the effect handler, which can then do some work and call <code>resume</code> to continue from the point that triggered the effect.</p>

<p>Let’s continue with the <code>Exception</code> example, but make it possible to recover from errors. The effect would be a little more complicated:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>interface Exception[T] {
  def throw(msg: String): [T]
}
</code></pre></div></div>

<p>Now we can throw an exception with a message, and the exception handler can give us back a value to use instead.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>val result = try {
  div(4, 0)
} with Exception {
  def throw(msg): Double = {
    println("oh no the div failed: " ++ msg)
    resume(42.0)
  }
}
println("finished: " ++ result)
</code></pre></div></div>

<p>The <code>div</code> function will be called, and it’ll again <code>throw</code> back to our exception handler. This time we print the error but then <code>resume</code> with a value. In <code>div</code> this is used as the result of the <code>do throw</code> expression.</p>

<p>In Koka this can get even more wild <a href="https://koka-lang.github.io/koka/doc/book.html#sec-multi-resume">where <code>resume</code> can be called more than once</a>. This forks off the original function so there are two instances, each progressing with different results. This is absolutely wild.</p>

<p>The key here is that you don’t have to call <code>resume</code> immediately. Just like how you can store a closure to compute some result later, you can wrap <code>resume</code> in a closure and wait to call it some other time. The state of the function that triggered the effect will be stored with the closure just like any other data. You can see this in action in the <a href="https://effekt-lang.org/examples/async.html">Effekt async example</a>.</p>

<h1 id="effects-versus-yield">Effects versus <code>yield</code></h1>

<p>That’s only just scratching the surface of how you can use effects for control flow. Something I found interesting while reading this is realising that Crystal’s <code>yield</code> keyword is just like a little baby effect system.</p>

<p>Crystal inherits the somewhat complicated block semantics from Ruby. This is exposed with the <code>Proc</code> type and the <code>yield</code> keyword. The simple example from the <a href="https://crystal-lang.org/reference/1.19/syntax_and_semantics/blocks_and_procs.html">documentation</a>:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def twice(&amp;)
  yield
  yield
end

twice do
  puts "Hello!"
end
</code></pre></div></div>

<p>The <code>yield</code> keyword yields (aahh!) control back to the calling function. In this example the block of code “passed” to <code>twice</code> is run two times. This is not too dissimilar to passing a <code>Runnable</code> to a Java method:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>void twice(Runnable block) {
  block.run();
  block.run();
}

twice(() -&gt; {
  System.out.println("Hello!")
});
</code></pre></div></div>

<p>Except the <code>yield</code> in Crystal is more powerful, because the caller can change the control flow in the function that accepts the block. You can <code>break</code> from within a block and cause an early return, or <code>return</code> from within the block and return from the method the block is in—not the method it’s calling.</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def find_mod_2(items)
  items.each do |i|
    if i % 2 == 0
      return i
    end
  end
end
</code></pre></div></div>

<p>That <code>return</code> statement will stop the execution of <code>each</code> <em>and</em> return from <code>find_mod_2</code>. If this was another language, or if <code>each</code> was implemented with a <code>Proc</code> rather than a <code>yield</code>, you would have to return a special value to indicate you wanted to stop, or raise an exception. This is how Crystal gets away with having no <code>for</code> loop in the language.<sup id="fnref:not-macros"><a href="#fn:not-macros" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> Otherwise the block would simply cede control to the method that called it.</p>

<p>What’s confusing is that you use the same syntax to create a <code>Proc</code> which <em>can’t</em> affect the control flow of the function that called it, and has the same limitations as other languages like Java. If you think about the implementation it makes sense, a <code>yield</code> cannot be stored and run later, outside of the execution of the method it is in, whereas a <code>Proc</code> can be stored as an instance variable and executed much later. Like, how would this work?</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Thingie
  getter block : Proc(Nil)? = nil

  def do_thing(&amp;block : Proc(Nil))
    puts "setting thing"
    @block = block
  end
end

def use_thingie(th : Thingie)
  th.do_thing do
    return "this is a value!"
  end
  puts "Am I unreachable?"
end

th = Thingie.new
use_thingie
th.block.call # what should happen here?
</code></pre></div></div>

<p>How can <code>use_thingie</code> ever finish if the <code>return</code> statement is in the <code>Proc</code>? What should happen when the <code>Proc</code> is called? It can’t return from <code>use_thingie</code> since that function will have already finished by the time it’s called.</p>

<p>The Crystal compiler knows this doesn’t work and the program will fail to compile:</p>

<pre><code>In test.cr:12:5

 12 | return "this is a value!"
      ^
Error: can't return from captured block, use next
</code></pre>

<p>This is the exact same distinction as <a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Escaping-Closures">Swift’s <code>@escaping</code> closures</a>, except Swift doesn’t allow control flow in non-escaping closures anyway.</p>

<p><code>yield</code> in Crystal is a very simple version of effects, since it will only allow jumping up one layer in the call stack, if you want to forward a block you need to re-<code>yield</code> when you call another function. There’s also only one possible receiver, the single block passed to the function will be used for all <code>yield</code> statements.</p>

<h1 id="dependency-injection-is-just-effects">Dependency injection is just effects</h1>

<p>You can only use an effect if somewhere up the call stack there is a place where that effect will be handled. In Java you need a <code>catch</code> around every <code>throw</code>, even if for runtime exceptions you can skirt around this slightly. In languages with <code>async</code>/<code>await</code> you must decorate a call to an <code>async</code> function with <code>await</code>, and the function you’re calling <em>from</em> must be <code>async</code>. Eventually up the call stack you’ll get to a call that adds the async work to a task queue, executor, or blocks waiting for it to complete. These are all examples of effect handlers for async programming. They provide the scheduling effects that the async code needs in order to run.</p>

<p>This can define lexical scopes; no code outside of places where a certain effect handler is installed may use that effect. My mind is broken in just the right way that when I realised this, I thought “that’s just dependency injection”.</p>

<p>The key of (<a href="https://dagger.dev">Dagger</a>-style) dependency injection is that you can only access certain dependencies in certain parts of the application, and how those dependencies are constructed is separated from their actual use. I like this so much I <a href="/2026/01/31/crystal-dependency-injection/">implemented it with Crystal macros</a>.</p>

<p>Since effects propagate up, they naturally support nested scopes. When an effect is triggered for a dependency provided by a wider scope, it will skip over the handler for the inner scope and jump straight to the outer handler to get the dependency.</p>

<p>This code is still fairly verbose, you would likely want some code generation or macros to tidy it up and make it less of a pain to write. We start with an injection effect:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>interface Inject[A] {
  def get(): A
}
</code></pre></div></div>

<p>As long as we have the right type annotations, we can <code>do get()</code> to defer to the effect handler to provide us with a value:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def functionWithDeps(): Unit / { Inject[Logger], Inject[Config] } = {
  val logger: Logger = do get()
  val config: Config = do get()
  logger.log("Doing stuff, this config: " ++ show(config))
  doImportantStuff(config)
}
</code></pre></div></div>

<p>This function can only be called in contexts where we can inject both a <code>Logger</code> and a <code>Config</code>. This is what the function call at the scope root would look like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def doWithInjection() = {
  val config = buildConfig()
  val logger = getLogger()
  try {
    functionWithDeps()
  } with Inject[Config] {
    def get() = resume(config)
  } with Inject[Logger] {
    def get() = resume(logger)
  }
}
</code></pre></div></div>

<p>This would obviously be unwieldy with lots of dependencies, but that could either be handled by clever type-system trickery, macros, or code generation. You’d also want to create the objects lazily, which I’ve neglected to do here.</p>

<p>What’s neat about this is that it works on functions rather than objects, so you’re not forced to indirect things through lots of different classes if you don’t want to.</p>

<h1 id="effect-syntax">Effect syntax</h1>

<p>Since many languages already have an effects-adjacent way of throwing and catching exceptions, the syntax changes required to support arbitrary effects are actually fairly minimal. Effects could slot quite nicely into the Swift syntax, at least the parts that I can think of. Since you want to stay as close to the existing <code>throws</code> and <code>async</code> keywords, I’d propose listing the effects between the argument list and the <code>-&gt;</code> before the return type, like this:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// No effects
func foo() -&gt; String {}
// One effect
func foo() async -&gt; String {}
// Two effects, one with a type parameter
func foo() throws&lt;Error&gt;, async -&gt; String {}
</code></pre></div></div>

<p>It doesn’t fit with other types in Swift, but I think the names of effects should be lowercase to appear more like “tags” than “types”, although I could be persuaded for consistency to uppercase them. Since an effect might have any number of generic parameters, you’d have to specify those within angle brackets which is a little ugly but not terrible.</p>

<p>Defining the effect would be similar to an enum definition:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>effect async {
  case suspend
  case cancel
}
</code></pre></div></div>

<p>This fits well because you’re yielding to a particular case in the effect, and you can add associated data to each <code>case</code> in just the same way you do for enums:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>effect throws&lt;T: Error&gt; {
  case throw(T)
}
</code></pre></div></div>

<p>Just like Effekt, I think the <code>do</code> keyword is nice to indicate that you’re doing something with an effect. I think this would be required on any function call that has an effect, like this:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>func fetchUserInfo(id: Int) async -&gt; User {
  let info = do userInfoLoader.load(id)
  return User(from: info)
}
</code></pre></div></div>

<p>At a point that we want to handle the effects, you would put a block that will match on the effects used in the <code>do</code> expression. Currently in Swift this is the <code>catch</code> keyword, but since this has to be more general I think <code>when</code> is a better fit. It would read as “do that, and when this happens, do this other thing”.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>do {
  do userInfoLoader.load(id)
} when throws(error) {
  Log.error("Unable to load user \(id): \(error)")
  return nil
}
</code></pre></div></div>

<p>If you need to handle multiple effects, you’d tack on extra <code>when</code> blocks just like you can with <code>catch</code> blocks today.</p>

<p>The case with <code>throws</code> would be—like Effekt—a special case for an effect with a single type. For effect handlers with multiple cases, the body of the <code>when</code> block would be equivalent to a <code>switch</code> statement:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>do {
  do asyncScheduler.doSomeWork()
} when async {
  case suspend: {
    self.pendingTasks.append {
      resume
    }
  }
  case cancel: {
    self.onTaskCancelled()
  }
}
</code></pre></div></div>

<p>What I think is interesting about this exercise is that from a syntactic point of view, there isn’t really that much to change. Functions can already be tagged with a fixed set of effects, and there’s already syntactic structures to handle them.</p>

<h1 id="do-you-want-effects">Do you want effects?</h1>

<p>I got into this mess because I was reading about ways of handling errors and also ways of handling async programming.</p>

<p>Much like generics, I don’t think most code would have to worry about defining their own effects or effects handlers. Having exceptions and <code>async</code>/<code>await</code> not be something that’s built into the language and instead be something that’s built <em>with</em> the language would be really cool. The language could be less prescriptive over how async code is written, perhaps allowing certain codepaths to have strict guarantees on how fibres can be cancelled, for example.</p>

<p>This might be the way to get structured concurrency into a language without placing the entire burden on the language itself. It would allow library authors to dictate the contexts in which certain functions could be called, enforcing structure and correctness. In most garbage-collected languages many contracts are only enforced in documentation saying “don’t hold onto a reference to this object”, which I’ve also <a href="/2024/09/05/implicit-lifetimes-and-undroppable-types/">written about before</a>.</p>

<p>Effects would also be valuable in code that deals with deadlines or other scoped data that is typically stored in dependency injection, thread local variables, or passed through function calls manually. Instead of constantly checking the deadline, you could just augment the existing suspension effect to fail at any suspension point when a deadline has run out. Any code that needs to operate with a deadline simply couldn’t be called from contexts without a deadline.</p>

<p>I’ve focussed mostly on how effects relate back to exceptions and async code, since those are control-flow constructs that I (and probably you) are most familiar with. I haven’t given much thought to what it would be like to write code where all I/O is handled through effects. If you had to annotate every single function and function call that you wanted to do I/O, I imagine that would get really tedious. If the language had good type inference on the required effects, then it might not be so bad.</p>

<p>When I <a href="/2023/10/31/how-i-learned-to-stop-worrying-and-love-concurrency/">wrote all about concurrency</a> I argued that all code should be async by default—like Go or Crystal—since there’s already so much implicit behaviour going on in your typical program, you might as well get low-cost I/O while you’re at it. I do think there are contexts where it’s useful to know that you aren’t going to suspend for an arbitrarily long amount of time, like a UI handler, and having the ability to write APIs that require no effects would allow these kinds of guarantees.</p>

<p>So there you go, I started out wanting to understand error handling and instead learnt that everything I know about programming could somehow be linked back to one language feature. If you want to read more, I’d recommend starting with the <a href="https://effekt-lang.org/">Effekt</a> and <a href="https://koka-lang.github.io/">Koka</a> language tours (I just skipped straight to the good bits).</p>

<p>Please take my explanations of how function calls, and exceptions work here as illustrative rather than literal. I wanted to give an example of the <em>kind</em> of thing the computer is doing without getting too bogged down in how the computer actually does it. The aim of this is to think more about how control flow works with different languages, rather than how you’d actually implement it.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:have-not-asked">
      <p>Ok I haven’t asked mine. <a href="#fnref:have-not-asked" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:architecture">
      <p>More or less, depending on architecture I guess? I’m not a CPU instruction set expert, but this is the general concept. <a href="#fnref:architecture" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:or-c">
      <p>The original version of this post incorrectly stated that C had no dynamic dispatch, but this is possible with function pointers, which can even be used to implement an object system. <a href="#fnref:or-c" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:ignoring-stacks">
      <p>Ignoring stack frames and suchlike and the fact <code>goto</code> can’t jump across functions. <a href="#fnref:ignoring-stacks" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:not-macros">
      <p>Well apart from the macro language, which is kinda separate. <a href="#fnref:not-macros" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2026/03/02/async-inject-and-effects/">Permalink</a>
&bull; March 2, 2026
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Async%20Programming%20Is%20Just%20@Inject%20Time">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Ruby Scripting Utilities</title>
        <pubDate>Sun, 08 Feb 2026 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2026/02/08/ruby-scripting-utilities/</link>
        <guid isPermaLink="true">https://willhbr.net/2026/02/08/ruby-scripting-utilities/</guid>
        <description><![CDATA[<p>I think I’m pretty good at shell scripting: I quote my variables, I know the difference between <code>$@</code> and <code>$*</code>, I know about checking that variables are set with the <code>?</code> suffix. I’ve spent a lot of time messing around with shell scripts, but I always feel like the script that I write is almost always dictated by what’s easy or possible to do in the shell.</p>

<p>The main reason I did this was for a self-inflicted concern for portability. If I only wrote shell scripts, I could have everything working on any platform, without having to install additional dependencies or build custom executables.</p>

<p>Last year I decided that I’d had enough, and I gave myself permission to assume that any computer I was actually using would have Ruby installed. This led me to create an easy way to <a href="/2025/06/08/using-ruby-in-shell-pipelines/">use Ruby expressions as replacements for <code>sed</code> or <code>awk</code></a>. I then <a href="https://codeberg.org/willhbr/dotfiles/commit/3dd076de57bf1bb29b4ab3018c70365623cebac5">replaced the script</a> that installs all my Zsh, tmux, and Vim plugins with a simple Ruby script.</p>

<p>Ruby has been my go-to scripting language for ages, but now I’ll skip straight past a shell script and go right to Ruby instead. I’ve been using it for one-off scripts as well as small utilities.</p>

<p>The biggest problem is that I constantly end up writing this function:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def run(*args)
  unless system *args
    raise "command failed: #{args.join ' '}"
  end
end
</code></pre></div></div>

<p>If you’re not fluent in Ruby, <code>system</code> runs a subprocess that inherits the IO of the Ruby process, and returns <code>true</code> or <code>false</code> depending on the exit status of the program. The way I almost always want it to work is to just stop the whole program if something goes wrong, so I always write out this little helper.</p>

<p>The next issue is that I’m really used to <a href="https://crystal-lang.org/api/latest/Process.html">Crystal’s <code>Process</code> class</a> that makes it really easy to manage subprocesses. Paired with Crystal’s event loop and <code>IO</code> module, it’s easy to read and write data to the program, or just spawn it and wait for it to finish.</p>

<p>You can do lots of stuff with Crystal’s <code>Process</code>, but in a script all I really want to do is:</p>

<ul>
  <li>Run a program and throw an exception if it fails</li>
  <li>Run a program and capture its output (also throwing an exception if it fails)</li>
</ul>

<p>My <code>run</code> method does the first. The second can almost be done with the special backtick <code>`</code> method:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>files = `ls`
</code></pre></div></div>

<p>This is great until you want to pass some arguments in, because it only supports string interpolation, not passing in separate arguments. That’s <em>fine</em> for a one-off script where I know the input, but I’d rather not worry unnecessarily about shell injection problems.</p>

<p>Ruby does have an alternative: <code>Open3.capture2e</code>. It’s not exactly the kind of fluent API I’d like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>require "open3"

output, status = Open3.capture2e('jj', 'commit', '-m', message)
if status.exitstatus != 0
  raise "jj command failed"
end
# =&gt; do something with output
</code></pre></div></div>

<p>So what I’ve done is make use of the <code>RUBYLIB</code> environment variable. It points to an additional place (or places) that Ruby will look when you <code>require</code> a file. Instead of having to bundle common code in a gem, or rely on writing out an absolute path to exactly where my common code is, I can just:<sup id="fnref:process"><a href="#fn:process" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>require "process"

output = Process.capture 'jj', 'commit', '-m', message
</code></pre></div></div>

<p>I’ve added the two methods for calling external programs that I’ve been wanting, and then of course added a little helper library for interacting with JJ repos. You can see them in <a href="https://codeberg.org/willhbr/dotfiles/commit/c013021466f53d000a07608f93d931f8f0311381">this commit</a> in <a href="https://codeberg.org/willhbr/dotfiles/src/branch/main/rubylib">my dotfiles repo</a>. The intention here isn’t to create something to be used by other people, it’s purely so I can write scripts more easily.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:process">
      <p>It’s probably not the best idea to call this <code>process</code>, and chucking this in the <code>RUBYLIB</code> path could definitely cause a weird problem at some point. I just didn’t want to have a non-obvious name. <a href="#fnref:process" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2026/02/08/ruby-scripting-utilities/">Permalink</a>
&bull; February 8, 2026
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Ruby%20Scripting%20Utilities">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Building Dependency Injection with Crystal Macros</title>
        <pubDate>Sat, 31 Jan 2026 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2026/01/31/crystal-dependency-injection/</link>
        <guid isPermaLink="true">https://willhbr.net/2026/01/31/crystal-dependency-injection/</guid>
        <description><![CDATA[<p>When I first came across dependency injection I was a sceptic. Surely we could just create objects the normal way instead of worrying about modules and bindings? Eventually though I realised how you’re actually just generating the boilerplate code that you’d need to pass the dependencies around manually, and writing that code yourself is a huge waste of time.</p>

<p>I then wondered how hard it would be to implement the whole thing using Crystal macros. The aim is to have all the dependency resolution happen at compile time, so any failures to find a dependency will result in a compilation failure, and you’re not paying the price of a hash lookup or something similar to find each dependency during construction.</p>

<h1 id="dependency-model">Dependency Model</h1>

<p>The model I’ve implemented is based on <a href="https://dagger.dev">Dagger</a> (and its cousin, Hilt) since that’s what I’ve used the most. How Dagger works is that every injectable type is “installed” in a component, and can access any dependency from that component or its parent component. In a Hilt Android app, this means that a dependency in the <code>ActivityComponent</code> can inject something from the <code>SingletonComponent</code>, but not the other way around.</p>

<p>Each injectable type also has a policy of whether a new object should be created each time, or whether Dagger should hold on to the object and share it between dependent classes. In Dagger these are “scopes” which everyone gets confused about because <code>ActivityScope</code> and <code>ActivityComponent</code> seem like they should be the same thing, but <code>ActivityScope</code> just says to keep the object for the life of the activity, and can only be used on dependencies in the <code>ActivityComponent</code>. The equivalent for <code>SingletonComponent</code> is just <code>Singleton</code>, which is confusing because it is a scope, but it’s not named that way.</p>

<p>I decided to not match the nomenclature and invent my own terminology. Each collection of dependencies is a <em>scope</em>, which may have a parent scope (and the parent scope may also have its own parent). Every injectable type must have either a <code>@[Retain]</code> or <code>@[Recreate]</code> annotation that denotes whether it should be held onto or not.</p>

<p>What I like about this model is that you are effectively adding some lifetimes to objects in a language that doesn’t actually support them.</p>

<h1 id="implementation">Implementation</h1>

<p>The first trick that makes this whole thing work is defining the scopes as classes. They could be defined by annotations or just instances of a single <code>Scope</code> class, but as you’ll see later this lets us trick the compiler into validating our dependency resolution at compile time for us. Doing that would be much harder if the scopes were defined another way.</p>

<p>In a simple HTTP server, we might have scopes that look like this:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class GlobalScope &lt; Scope
end

class RequestScope &lt; SubScope(GlobalScope)
end
</code></pre></div></div>

<p><code>GlobalScope</code> holds everything that is accessible anywhere in the application and lives until the process exits, then <code>RequestScope</code> holds things that are only relevant to a single incoming request and will be discarded once it has been handled.</p>

<p>Don’t worry about <code>SubScope</code>—we’ll get to that later.</p>

<p>Now the main thing we need is to generate the code that builds our object and its dependencies. There are a few ways of defining the macro to do this—you could define a macro on a module and call that from inside the class—but what I ended up doing was creating a generic module with <a href="https://crystal-lang.org/reference/latest/syntax_and_semantics/macros/hooks.html">an <code>included</code> hook</a>. The actual code looks like this:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class RequestProcessor
  include Injectable(RequestScope)

  ...
end
</code></pre></div></div>

<p>Using a generic module means that at compile time we have access to <code>T</code>—the type of the scope—and <code>@type</code>—the class we’re building the injector for. Other approaches could get the same thing, but it fits really nicely with the generic module.</p>

<p>In the module we define a hook:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>module Injectable(T)
  macro included
    ...
  end
end
</code></pre></div></div>

<p>The macro hook is invoked immediately on <code>include</code>, but we can do the classic trick of defining a <code>finished</code> macro <em>inside</em> that macro that calls another macro. That way we can run code after the whole class has been defined.</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>macro included
  {% verbatim do %}
    macro finished
      build_injector
    end
  {% end %}
end
</code></pre></div></div>

<p>I’ve written so many macros that I don’t even hesitate at the idea of a macro defining a macro that calls a macro. That’s just how I write code.</p>

<p>That <code>build_injector</code> macro does the actual work to generate the code. This happens in a few stages; I didn’t want to be overly prescriptive on which method would be called for injection, so you have to annotate it with <code>@[Inject]</code>, which means we first need to find the right method. This is a bit clumsy in a macro:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% method = nil %}
{% for m in @type.methods %}
  {% if m.annotation(Inject)
       unless method.nil?
         method.raise "multiple @[Inject] methods: #{m.name} and #{method.name}"
       end
       method = m
     end %}
{% end %}

{% if method.nil?
     @type.raise "no @[Inject] annotated method on #{@type}"
   end %}
</code></pre></div></div>

<p>After that we have <code>method</code>, which is a <a href="https://crystal-lang.org/api/latest/Crystal/Macros/Def.html"><code>Def</code></a> object. Looking at the arguments to a particular method is easier than trying to process instance variables, and only doing constructor injection instead of field injection (in Dagger parlance) gives a bit more flexibility to the class.</p>

<p>The next trick is to use the unsafe <code>.allocate</code> method to grab some uninitialised memory where we can put our object. We then just call the right <code>initialize</code> method (as defined by <code>method</code>) which will set the instance variables. That’s just a matter of generating a method call from the information in <code>method</code>:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>instance = {{ @type }}.allocate

instance.{{ method.name }}(
  {% for arg in method.args %}
    {{ arg.restriction }}.inject(scope),
  {% end %}
)
</code></pre></div></div>

<p>This can be a bit hard to parse, so let’s look at an example. If we have this class:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class RequestProcessor
  include Injectable(RequestScope)

  @[Inject]
  def initialize(
    @params : URI::Params,
    @context : HTTP::Context,
  )
  end
end
</code></pre></div></div>

<p>Then the generated <code>inject</code> method would look like this:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def inject(scope)
  instance = RequestProcessor.allocate

  instance.initialize(
    URI::Params.inject(scope),
    HTTP::Context.inject(scope),
  )
  return instance
end
</code></pre></div></div>

<p>Note that <code>@type</code> is expanded to <code>RequestProcessor</code> in the macro.</p>

<p>Now, this doesn’t actually work because we need to be able to retain objects in the scope, so that if they’re injected in two places, both will get the same instance. What we’ve got currently will just make a new instance of every object every time anything is injected, which isn’t very useful.</p>

<p>In this <code>inject</code> method the initial change is quite simple; instead of calling the <code>inject</code> method on each class to create an argument, read it from the scope—that’s why we have the scope in the first place. We’ll assume that the scope has a <code>.get</code> method, and the change is fairly simple:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>instance = {{ @type }}.allocate

instance.{{ method.name }}(
  {% for arg in method.args %}
    scope.get({{ arg.restriction }}),
  {% end %}
)
</code></pre></div></div>

<p>The problem is that we now have to go and write that <code>.get</code> method. Working out how to do this took some serious head scratching. The problem is that we need to generate some code that will look at the type that’s passed in, find out if it should be recreated or retained (and whether there’s an existing retained instance), then return the retained instance or create a new one.</p>

<p>Perhaps the most naïve way of doing this would be to have a <code>Hash(Class, Object)</code> that stores the objects, but Crystal doesn’t support using <code>Class</code> <em>or</em> <code>Object</code> as type constraints for instance variables, so that’s not an option.</p>

<p>I fiddled around with trying to do horrible unsafe things with <code>pointerof()</code> but that didn’t really get anywhere because even if I can store pointers to objects, I still need to know if I even need to store them in the first place.</p>

<p>I even thought about calling a specially-formatted method that would be handled by <a href="https://crystal-lang.org/reference/latest/syntax_and_semantics/macros/hooks.html"><code>method_missing</code></a>, parsed back into a type, and somehow worked it out from there.</p>

<p>In the end the solution was much simpler; all it took was a realisation of how redefining classes and namespaces work.</p>

<p>If you do this in Crystal, you will add a new method to the <code>String</code> class:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class String
  def sup
    "Sup, #{self}"
  end
end
</code></pre></div></div>

<p>But if you do this, you will define an entirely new class called <code>Geode::String</code> that is unrelated to the top-level <code>String</code> class:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>module Geode
  class String
    def sup
      "Sup, #{self}"
    end
  end
end
</code></pre></div></div>

<p>That works the same if <code>Geode</code> is a module, class, or struct.</p>

<p>I thought that because my macro was generating code that lived inside the to-be-injected class, I couldn’t patch new methods into other classes, so I couldn’t define a new field on <code>RequestScope</code> from within that macro.</p>

<p>In Crystal every type is resolved relative to the current module or class, so within <code>Geode</code> if you wrote <code>String</code> you’d get your custom class, not the actual string class. If you want to be unambiguous, you can prefix the type with double colons to turn it into an absolute path (just like in C++). So in <code>Geode</code> you can use <code>::String</code> to refer to the actual string class.</p>

<p>What I didn’t realise is that you can do this same thing when you’re patching a class. So from within one class, you can patch a class in an outer module just by passing an absolute path. Since we have the type of the scope in our generic module as <code>T</code>, we can patch the class like this:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class ::{{ T }}
  {% if @type.annotation(::Retain) %}
    @var_{{ @type.id }} : {{ @type }}? = nil

    def get_{{ @type.id }} : {{ @type }}
      @var_{{ @type.id }} ||= {{ @type }}.inject(self)
    end
  {% else %}
    def get_{{ @type.id }}
      {{ @type }}.inject(self)
    end
  {% end %}
end
</code></pre></div></div>

<p>This checks whether our type needs to be retained, then either generates an instance variable and getter method, or just a getter method.</p>

<p>The getter method will be named something like <code>get_RequestProcessor</code>. I didn’t include it in the example, but since we can have generic types or types nested in modules, we actually need to strip any special characters out of the type name, like this: <code>@type.stringify.gsub(/[():]/, "_").id</code>. Since Crystal macros don’t have methods, every time we want to access the specially-named getter, we have to duplicate that snippet.</p>

<p>Now we’re able to store the object, and we’ve got a method to access it, but we still don’t have our <code>.get</code> method. This requires another few tricks that will interact with our specially-named method.</p>

<p><a href="https://crystal-lang.org/reference/latest/syntax_and_semantics/macros/macro_methods.html">Macro methods</a> get instantiated separately for every type that calls them—at least conceptually. We can capture that type in a generic parameter and then call <code>.get_{{ T.id }}</code> to either get the retained instance or a new object:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def get(cls : T.class) : T forall T
  {% begin %}
    {% name = T.stringify.gsub(/[():]/, "_") %}

    {% if @type.has_method? "get_#{name.id}" %}
      self.get_{{ name.id }}
    {% else %}
      {% T.raise "#{T} not registered in #{@type}" %}
    {% end %}
  {% end %}
end
</code></pre></div></div>

<p>I’m actually combining this with <code>has_method?</code> in order to fail with a more sensible error message. This is how the compiler is tricked into doing our dependency resolution at compile time: it needs to generate all the instantiations of this <code>.get</code> method, and if it can’t call the right getter, then we’ve got an object that can’t be constructed through dependency injection.</p>

<p>Although that’s still only half the story. I promised earlier that I’d get to <code>SubScope(T)</code>, and this is where that comes in. Since scopes are organised in a hierarchy, in order to have the compiler do type checking, that hierarchy needs to be represented in the type system. By making <code>SubScope(T)</code> a generic class, the child scope can have a concrete reference to its parent type, and the fallback <code>.get</code> method call is directly on the parent scope type, rather than being a dynamic dispatch on the <code>Scope</code> superclass.</p>

<p>That’s a lot to take in, so here’s the code then we can go through an example.</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def get(cls : T.class) : T forall T
  {% begin %}
    {% name = T.stringify.gsub(/[():]/, "_") %}

    {% if @type.has_method? "get_#{name.id}" %}
      self.get_{{ name.id }}
    {% else %}
      @parent.get(cls)
    {% end %}
  {% end %}
end
</code></pre></div></div>

<p>Let’s say we have <code>RequestLogger</code> which is in <code>RequestScope</code>, and <code>Logger</code> which is in <code>GlobalScope</code>. <code>RequestLogger</code> injects the <code>Logger</code>. How does that dependency get resolved?</p>

<p>The <code>Injectable(T)</code> module macro will generate the <code>inject</code> method, which will build the call to <code>initialize</code>:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def inject(scope)
  instance = RequestLogger.allocate

  instance.initialize(
    scope.get(Logger),
  )
  return instance
end
</code></pre></div></div>

<p>In this method, <code>scope</code> is a <code>RequestScope</code>. The Crystal compiler sees that we’ve called <code>.get</code> with a parameter of type <code>Class(Logger)</code>, and it generates a new specialisation of that method for us, and runs our macro code in that method.</p>

<p>The <code>@type.has_method?</code> check returns <code>false</code>, since <code>Logger</code> is registered in the <code>GlobalScope</code>, and so there’s no <code>get_Logger</code> method patched into the <code>RequestScope</code>. The code generated for <code>.get</code> looks like this:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def get(cls : Logger.class) : Logger
  @parent.get(Logger)
end
</code></pre></div></div>

<p><code>@parent</code> is defined in the initialiser for <code>SubScope(S)</code> as being of type <code>S</code>:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class SubScope(S) &lt; Scope
  def initialize(@parent : S)
  end
end
</code></pre></div></div>

<p>Since <code>RequestScope</code> inherits from <code>SubScope(GlobalScope)</code>, the compiler knows that <code>@parent</code> <em>must</em> be a <code>GlobalScope</code>, and so it knows it needs to create a specialisation on that type to satisfy this <code>.get(Logger)</code> call.</p>

<p>In this case, <code>GlobalScope</code> is a regular <code>Scope</code> and so the generated method is slightly different. It does the check for whether there’s a <code>get_Logger</code> method defined—in this case there is, so it will delegate to that. If there wasn’t, then it will raise an exception and compilation will fail.</p>

<p>If we put enough <code>@[AlwaysInline]</code> annotations on these methods, then the <code>get(Logger)</code> call in <code>RequestLogger</code> <em>should</em> be able to skip right to reading the field from whichever scope holds the logger, without doing any method dispatches. Talk about zero-cost abstraction.</p>

<h1 id="provider-and-lazy"><code>Provider</code> and <code>Lazy</code></h1>

<p>Any good dependency injector will know you can’t inject dependencies without deferring their construction in some cases. This lets you do something like:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class MyFeature
  include Injectable(GlobalScope)

  @database : Database

  @[Inject]
  def initialize(
    config : Config,
    old_database : Provider(OldDatabase, GlobalScope),
    new_database : Provider(NewDatabase, GlobalScope)
  )
    if config.use_new_database?
      @database = new_database.get
    else
      @database = old_database.get
    end
  end
end
</code></pre></div></div>

<p>The implementation for these is fairly straightforward:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>struct Provider(T, S)
  def self.inject(scope : S)
    new(scope)
  end

  def initialize(@scope : S)
  end

  def get
    @scope.get(T)
  end
end
</code></pre></div></div>

<p>The biggest challenge here was that originally I didn’t have them being generic over the scope, which meant the type of <code>@scope</code> was <code>Scope+</code> (any <code>Scope</code> subclass), and when the <code>get(T)</code> method was instantiated, it would be instantiated on the top-level class, which wouldn’t have the special getter method defined, and the dependency resolution would fail. Making them generic on the scope adds a bit of complexity, but it’s necessary to have the resolution work at compile time.</p>

<p>Both of these classes have to be special-cased into the construction of the objects, which is a little messy. The actual call to the <code>initialize</code> method looks like this:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>instance.{{ method.name }}(
  {% for arg in method.args %}
    {% if arg.restriction.nil?
         arg.raise "needs restriction on #{arg}"
       end %}
    {% if arg.restriction.resolve?.nil?
         arg.raise "Unable to resolve #{arg.restriction}"
       end %}

    {% if arg.restriction.resolve? &lt; Provider %}
      {{ arg.restriction }}.inject(scope),
    {% elsif arg.restriction.resolve? &lt; Lazy %}
      {{ arg.restriction }}.inject(scope),
    {% else %}
      scope.get({{ arg.restriction }}),
    {% end %}
  {% end %}
)
</code></pre></div></div>

<p>This will always create a new <code>Provider</code> or <code>Lazy</code> instance (neither wrapper type should be retained) and then the actual object creation is done in <code>Provider#get</code> and <code>Lazy#get</code>, which call <code>@scope.get(T)</code>.</p>

<h1 id="partialinjectable"><code>PartialInjectable</code></h1>

<p>The other classic dependency injection pattern is a class that has some fields injected, and some fields passed in explicitly. Usually this is done by generating a factory class, which is exactly what I did. It was quite satisfying to be able to generate code that would invoke the macro that I’d just written to then generate more code, and have it all fit together.</p>

<p>The macros are fairly similar to the rest, it just generates an inner struct named <code>Factory</code> and uses the <a href="https://crystal-lang.org/api/latest/Crystal/Macros/Def.html#splat_index:NumberLiteral%7CNilLiteral-instance-method"><code>splat_index</code></a> to differentiate between injected arguments and regular arguments. The injected arguments are each automatically wrapped in a <code>Provider</code>, so that the dependencies are only resolved when <code>new</code> is called on the factory.</p>

<h1 id="qualifiers">Qualifiers</h1>

<p>If you want to inject two objects of the same type in Dagger <a href="https://dagger.dev/semantics/#keys">you can use annotations</a> to differentiate them. I thought about using annotations in Crystal to do the same thing, but decided against it as it would be a bunch more work and complexity. Instead you can implement a single-field struct that wraps the type you want to duplicate—I even made a convenient macro for it.</p>

<h1 id="scope-parameters">Scope Parameters</h1>

<p>For subscopes that correspond to a particular action (like an HTTP request) you need to be able to put values into the dependency graph. In terms of code, this is as simple as defining the right method on the scope so that the <code>has_method?</code> check in the <code>get</code> macro method will find it.</p>

<p>I made a macro that helps defining these, since writing them manually would be messy.</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class RequestScope &lt; SubScope(GlobalScope)
  params [
    context : HTTP::Server::Context
  ]
end
</code></pre></div></div>

<p>Then when you make the scope, you pass in the parameters: <code>global.new_request_scope(context: context)</code>. These will then be available to inject to any class in that scope.</p>

<h1 id="using-it">Using it</h1>

<p>Here’s a somewhat contrived, overly simple example of where this might be useful. We’ve got two scopes just like the rest of the post, a global config, and a processor that is only used for one request.</p>

<p>Here are the scopes and config:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class AppConfig
  getter do_stuff : Bool
end

class GlobalScope &lt; Scope
  params [
    config : AppConfig
  ]
end

class RequestScope &lt; SubScope(GlobalScope)
  params [
    context : HTTP::Server::Context
  ]
end
</code></pre></div></div>

<p>Here’s the actual interesting stuff. What’s neat is that we don’t have to plumb <code>Logger</code> or <code>AppConfig</code> into the <code>Handler</code>, and if we decide that <code>Logger</code> should be request-scoped in order to log with embedded request information, we can just move it into that scope and not have to do all the rewiring.</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@[Retain]
class RequestProcessor
  @[Inject]
  def initialize(
    @context : HTTP::Server::Context,
    @logger : Logger,
    @config : AppConfig
  )
  end

  def process
    @logger.log { "Processing request!" }
    ...
    if @config.do_stuff
      write_data(@context)
    end
  end
end

@[Retain]
class Handler
  include HTTP::Handler

  @[Inject]
  def initialize(@global : GlobalScope)
  end

  def call(context)
    req_scope = @global.new_request_scope(context: context)
    processor = req_scope.get(RequestProcessor)
    processor.process
  end
end
</code></pre></div></div>

<p>Now we just need to set up the application by loading the config, creating the root scope (and passing the config in), and starting our server. The server could also be constructed by dependency injection if we really wanted.</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>config = Config.load_from_file

global_scope = GlobalScope.new(config: config)
server = HTTP::Server.new [global_scope.get(Handler)]
server.bind_tcp "0", 80
server.listen
</code></pre></div></div>

<h1 id="but-why">But Why</h1>

<p>Surprisingly I didn’t do this <em>entirely</em> for my own amusement, I did actually have a use case that dependency injection would have made easier. I made some changes to <a href="https://codeberg.org/willhbr/ssh-honeypot">my SSH honeypot</a> that I wrote years ago so that instead of just logging the commands, it would run them in a container. It would hash the username, password, and remote address to create a container name so that repeated connections would be run in the same container.</p>

<p>I then wanted to change this so that I could optionally share containers based on some logic. I would usually do this by having a <code>ContainerDispatcher</code> or something that would be owned by the SSH server, and pass a reference to it to each <code>PodmanConnection</code> when a client was connected. The <code>PodmanConnection</code> would ask the dispatcher for a container, and since it would have visibility into all running containers, it could return an existing one or create a new one.</p>

<p>This is a tiny microcosm of where dependency injection is useful. The connection should exist in its own scope, and request a dispatcher from <em>somewhere</em>. It doesn’t care if that dispatcher is in the same scope or in the parent scope—it just says “give me the thing that will let me get containers”. The SSH server doesn’t really need to know that the dispatcher should be shared among all connections, it doesn’t even really need to <em>know</em> about dispatching between containers at all.</p>

<p>I know dependency injection is a hallmark of over-architected enterprise software, but in cases like these I think it’s a useful tool.</p>

<p>You can see the <a href="https://codeberg.org/willhbr/geode/src/branch/main/src/geode/dependency_injector.cr">full implementation</a> in <a href="https://codeberg.org/willhbr/geode">Geode</a>. If I end up using this in my projects, no doubt I’ll make some changes as I find pain points and other shortcomings.</p>


<a href="https://willhbr.net/2026/01/31/crystal-dependency-injection/">Permalink</a>
&bull; January 31, 2026
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Building%20Dependency%20Injection%20with%20Crystal%20Macros">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Why Containers?</title>
        <pubDate>Thu, 15 Jan 2026 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2026/01/15/why-containers/</link>
        <guid isPermaLink="true">https://willhbr.net/2026/01/15/why-containers/</guid>
        <description><![CDATA[<p>There seems to be a recurring sentiment that pops up every now and again on Mastodon: “this project looks interesting, but the installation instructions say to use Docker, so I’m not interested anymore”. Now I totally understand the sentiment. If I came across a project and the instructions said to install it I’d need <a href="https://nixos.org">Nix</a>, I would also do a quick 180.</p>

<p>However, I’ve been using containers for development and for the services I run at home for a few years now, and I quite like it. So I thought it might be interesting to explain what I get out of containers, some of the bad bits, and right at the end some feelings about the mismatch between container enthusiasts and container skeptics.</p>

<h1 id="use-podman">Use <code>podman</code></h1>

<p>I’ll get this out of the way: I don’t actually use Docker, I use Podman. Aside from being more permissively licensed, it’s also much easier to install on basically any Linux system as it’s available in most distros’ package repository. On Ubuntu I can just <code>apt install podman</code> and on Alpine I can <code>apk add podman</code>. The <a href="https://docs.docker.com/engine/install/ubuntu/">Docker installation instructions</a> are <em>much</em> more involved.</p>

<p>This is the main reason I would recommend Podman, maybe second only to the fact that Podman runs rootless by default, which reduces the chances of a rogue container stomping over something on your machine that it shouldn’t.</p>

<h1 id="process-isolation">Process isolation</h1>

<p>By far the biggest benefit of using containers for me is low-effort process isolation. I don’t like the fact that if I run something myself it’ll have access to all the data on my system and be able to read and upload it, or simply just corrupt or delete it.</p>

<p>You can of course get this same benefit by running services as different users, which most well-behaved services installed by system packages should be doing anyway.</p>

<p>Running services as different users does make sharing data between services more difficult, and I’m not very good at managing groups. Being able to just restrict the paths that the container has access to is really simple both to do and conceptually. I don’t have to think about who is a member of which group and what access that grants them, I just say “you can access this folder” and that works really well for me.</p>

<p>The biggest benefit of container isolation is that when the container is gone, everything else goes with it. If the process was writing logs, config, temporary files, whatever, it’s all constrained into the container. Once I’ve done <code>podman rm</code> I know that everything belonging to that container has been purged.</p>

<p>This makes trying new software a lot more like apps on a phone; you know that the risk of installing is very limited, and when you uninstall you know that everything gets removed. The exact opposite of installing software on Windows XP, where you’d have to go through a wizard, it would require full admin access, and it could make any change to your machine it wanted.</p>

<p>This isolation can also flip who is in control of the program. For example, some program might only support listening on a particular port or saving its data to a particular directory. This is of course bad software. Nevertheless I have the power to say “no thanks” and re-map the port and filesystem locations so from my perspective the program is working the way I want.</p>

<h1 id="opaque-storage">Opaque storage</h1>

<p>Since I’m in full control of what data in the container gets persisted, I have much more control about where it actually lives on my system, and thus how it gets backed up.</p>

<p>All the containers that actually store data—most important being <a href="https://www.freshrss.org">FreshRSS</a>—I map their data into a Podman <a href="https://docs.podman.io/en/v4.4/volume.html">volume</a>, which I can treat as an opaque blob. Currently I have a script that will take these volumes, export them to TAR files, and upload them to my NAS.</p>

<p>I’m definitely missing out on incremental backup here, as the TAR file requires a full upload each time. This is not perfect.</p>

<h1 id="processes-versus-programs">Processes versus programs</h1>

<p>I like the fact that I can think of my programs more like programs instead of having to deal with OS processes. Perhaps I’d get this same benefit if I was good with systemd, but I’m not, so here we are. Instead of haphazardly calling <code>kill</code> with copy-pasted process IDs, I can just <code>podman stop</code> or <code>podman restart</code> with the container name—and it’s a name that I can choose myself.</p>

<p>This grouping of processes also exposes better monitoring—on Ubuntu, at least. I use <a href="https://github.com/containers/prometheus-podman-exporter"><code>prometheus-podman-exporter</code></a> to grab metrics for each container, so I can see where I’m spending my RAM. Annoyingly, due to <a href="https://github.com/containers/podman/issues/9502">some cgroups issue on Alpine</a> you just get aggregated stats, not per-container stats. So I can’t actually take advantage of this for containers on my Alpine servers.</p>

<h1 id="remote-management">Remote management</h1>

<p>Something I like about Podman specifically is <a href="https://docs.podman.io/en/latest/markdown/podman-remote.1.html">podman-remote</a>. I’ve <a href="/2023/05/05/setting-up-podman-remote/">written about this before</a>, it’s the secret behind <a href="/2025/03/10/endash-a-lightweight-container-dashboard/">my container dashboard</a>.</p>

<p>Since I can interact with the containers on my local machine in just the same way as I do the ones on my other servers, it is really easy to write scripts that deal with both transparently. Endash merges containers running on my two home servers and a VPS into one interface, even with a mix of Ubuntu and Alpine between them.</p>

<p>Even just for local containers, having an interface to get details about what’s running in a computer-readable JSON format is really convenient. With Endash I can just list the currently running containers and get the ports that they’re listening on to include as links in the dashboard. I can introspect the volumes they have access to, how long they’ve been running, and more all from one API.</p>

<h1 id="the-rule-of-two">The rule of two</h1>

<p>This is more geared towards development, but it is helpful for any kind of debugging. Since the process running in the container has no idea about what’s happening in the real world, it’s really quick and easy to run a second instance of some service.</p>

<p>All I have to do is assign it a different port and mount a different directory—or mount no directory to get a fully clean environment—and then I can have both running in parallel.</p>

<p>Paired with the fact that all the data can be isolated to a particular volume, I could duplicate the volume, run a new container pointing to the second copy of my data, and experiment with some alternative configuration or a major version upgrade. All with the peace of mind that if it doesn’t work out, I haven’t actually done anything to my actual service.</p>

<h1 id="dependency-heaven">Dependency Heaven</h1>

<p>Dependency hell isn’t particularly likely if you’re installing the mainstream versions of packages from the package manager that comes with your distro. People have <a href="https://unnamed.website/posts/installing-every-arch-package/">managed to install a lot of packages at once</a>.</p>

<p>I don’t think I actually run enough services for this to be a serious issue, but in theory you could have services that require mutually incompatible versions of shared libraries, command-line tools, or suchlike. Being able to run these without messing with load paths or <code>$PATH</code> is convenient.</p>

<p>The biggest advantage of this dependency isolation is development, where you can hack around with the system safe in the knowledge that you won’t break something important.</p>

<h1 id="development">Development</h1>

<p>I <a href="/2023/06/08/overcoming-a-fear-of-containerisation/">originally came to containerisation</a> as a way to control my development environment, rather than a way to run things on my home server. For development, using <code>podman</code> directly kinda stinks, the commands are exceptionally verbose and easy to get wrong. So much so that I <a href="/2023/06/08/pod-the-container-manager/">wrote my own program to make this easier</a>.</p>

<p>Over two years later and I’m still mostly working this way, but I did walk it back a little. I use <code>cargo</code> directly on the system and I gave up on using containers for one-off scripts as it was just too much friction. But containers have made it exceptionally easy for me to write a little server, package it up, and keep it running on my home server. I’ve currently got 6 of my own containers running across 3 different servers. Running, deploying, and monitoring them is straightforward.</p>

<p>The other development advantage is being able to hack around in a safe sandbox to try and get something working. I try not to install too much weird stuff onto my development machine, especially old tools that might conflict with a more recent version that I rely on. If I containerise this, I can do almost anything without affecting my actual machine.</p>

<p>As a concrete example, I recently wanted to find out why <a href="https://jekyll.github.io/jekyll-admin/">jekyll-admin</a> would show an error on basically every page load. To do this I needed to build the project, it’s built with React which means installing <a href="https://nodejs.org/en">node.js</a> and <a href="https://yarnpkg.com">Yarn</a>. Some part of this build process requires a Python interpreter, and the versions that jekyll-admin requires are old enough that it would only work with Python 2.<sup id="fnref:jekyll-end"><a href="#fn:jekyll-end" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> Now there’s no way that I’d pollute my computer with a Python 2 install, but in a container I can hack around with whatever I want, knowing that it can all just disappear once I’m done.</p>

<p>I did the same thing while trying to work out <a href="/2025/10/13/the-6-2nd-stage-of-swift-on-linux/">why I still couldn’t use websockets in Swift</a>. Firstly Swift only supports a few Linux distros,<sup id="fnref:linux-swift-support"><a href="#fn:linux-swift-support" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> so being able to fake Ubuntu from within Alpine is a useful trick, but also the availability of the libcurl version depended on the underlying Ubuntu version, and I could just swap between two versions—or even run both at the same time—trivially.</p>

<p>Running code in containers also forces you to understand what implicit dependencies you’re pulling in, usually development headers for some library, the <code>build-essential</code> package, or maybe just <code>tzdata</code>. Usually these are things that you install and forget about, then when you go to help someone else it’s really hard to remember what you need to do, or even to know what you need in the first place. Having a containerfile doesn’t guarantee reproducibility—it might reference an image that changes—but at least there’s some intention there that can be reverse-engineered.</p>

<h1 id="the-bad-bits">The bad bits</h1>

<p>If you use containers, then you need to trust whichever registry you pull from just like you trust the system package manager. I don’t know much on the relative security tradeoffs, but you’re probably running a newer version if you pull from a registry, which might have unfound flaws. The package manager maintainers might be altering or re-building packages in a way that you find valuable, but they also might be doing something you disagree with.</p>

<p>Trust is definitely something I have in the back of my mind. I don’t have a particularly robust approach here, but if there isn’t a prebuilt image that I feel comfortable with, I’ll just build my own from a standard OS image (probably Alpine) and install the package using <code>apk</code> or <code>apt</code>. That way I’m still getting a container, but using the package from the distro.</p>

<p>The <code>podman</code> CLI is complicated. Somewhat necessarily so, given the amount of stuff it can do, but nonetheless it is really an interface best used by computers, not by people—which I’ll get to more at the end.</p>

<p>Podman networking (or container networking in general) is terrible. I’ve spent ages trying to understand what you can and cannot do, only to run into dead ends and <a href="/2025/11/30/alpine-containers-forget-tailnet/">hard-to-debug problems</a>. The key to a happy life is to set <code>ufw</code> to block all incoming connections outside of port 22, 80, and 443, have containers bind to particular known ports, and use <a href="https://caddyserver.com">Caddy</a> as a proxy.</p>

<p>The opacity you get from using volumes as storage can turn around and bite you when you want to quickly grab something from a volume. The experience is about as good as trying to <code>scp</code> something without knowing the exact path. The less I have to do this the better, and if I’ll need to look at the files with any regularity I’ll use <a href="https://docs.podman.io/en/v4.4/markdown/options/mount.html">a bind mount instead</a>.</p>

<h1 id="im-definitely-over-invested">I’m definitely over-invested</h1>

<p>You’ve probably realised now that I’m over-invested. I’ve picked containers as the backbone of how I use computers and there’s no going back now. I’ve <a href="/2023/06/08/pod-the-container-manager/">written a tool for managing them</a> and then <a href="/2025/07/25/rewriting-pod-with-wisdom/">rewritten it in Rust</a> and <a href="/2025/03/10/endash-a-lightweight-container-dashboard/">written a custom web interface to view my containers</a>. Without this, I’m lost.</p>

<p>This has definitely given me an appreciation for what you can do with containers, moreso than if I’d pasted in a few <code>podman run</code> commands to get something working. A large part of the reason why I wrote these tools was because I <em>didn’t</em> understand containers, and the way I get an understanding is to get right down into the weeds. I wouldn’t be as comfortable using containers as heavily as I do without having my own system around them.</p>

<h1 id="putting-it-all-together">Putting it all together</h1>

<p>Just to make it super clear, I have a custom tool to build, run, deploy, and update containers, just because I didn’t like any of the existing tools. In the shipping container analogy, I’ve built my own boat, harbour, dock, and crane system. So if you find yourself thinking that a particular podman command is hard to remember or complicated, just know that I’ve spent hours writing thousands of lines of code to manage that complexity.</p>

<p>Physical shipping containers aren’t very useful if you don’t have a ship that’s built to transport them. In fact it’s more difficult to cram shipping containers onto a pre-containerisation vessel than it would be to just carry the cargo directly.</p>

<p>That’s the key really, I’ve built a whole system with containers as the foundation, which has made it really easy for me to use containers. But podman doesn’t come with a system, it’s just a box you can put stuff in, and that’s only half the answer.</p>

<p>To get real value out of containers you need a vessel, whether that’s <a href="https://docs.podman.io/en/latest/markdown/podman-compose.1.html"><code>podman-compose</code></a> or <a href="https://kubernetes.io">Kubernetes</a>, you need something that will let you take advantage of everything being the same shape.</p>

<p>For someone that just wants to run something on their server that they administer themselves, these tools are a whole new system to the way they’re used to working. When a project says “run this with Docker” what they’re doing is asking you to fit a 40 foot container onto a dinghy.</p>

<p>I think that containers at this scale should be treated as a tool instead of a whole system; developers can package their application and its dependencies in a standard way across all distros, and have it run in the same environment without needing to adapt to the actual machine it’s running on. It’s then up to the distro to provide a way for the user to manage the application <em>without</em> knowing it’s backed by a container. Let the container be a packaging tool.</p>

<p>With that in mind, if you’re still reading, the tool that gets as close to this as possible is <a href="https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html">podman quadlets</a>, which let you run containers as systemd services. You still have to know they’re backed by a container, but it can be managed the same way as other services on your system.<sup id="fnref:quadlet-reading"><a href="#fn:quadlet-reading" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:jekyll-end">
      <p>The end result was that I tried updating some of the dependencies to work with the current version of Node, realised it would be a serious effort, then gave up. That would have happened with or without containers though. <a href="#fnref:jekyll-end" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:linux-swift-support">
      <p><a href="https://www.swift.org/platform-support/">Ubuntu, Debian, Fedora, RHEL, and Amazon Linux</a> at the time of writing. <a href="#fnref:linux-swift-support" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:quadlet-reading">
      <p>I’d <a href="https://matduggan.com/replace-compose-with-quadlet/">read this</a> as a nice overview of how and why to use quadlets. <a href="#fnref:quadlet-reading" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2026/01/15/why-containers/">Permalink</a>
&bull; January 15, 2026
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Why%20Containers?">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Upgrading to Jekyll 4.4</title>
        <pubDate>Sat, 03 Jan 2026 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2026/01/03/upgrading-to-jekyll-4-4/</link>
        <guid isPermaLink="true">https://willhbr.net/2026/01/03/upgrading-to-jekyll-4-4/</guid>
        <description><![CDATA[<p>This all started as I was getting confused about <a href="/2025/12/24/my-website-broke-and-you-wont-believe-why/">how syntax highlighting broke</a> on my website, and my confusion as to what GitHub could have possibly done to break it. As it turns out they didn’t do anything, but the idea of moving to an <a href="https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages">actions-based website</a> seemed less daunting.</p>

<p>Originally GitHub Pages only supported one way of building a website, which was with Jekyll and a fixed set of plugins. You couldn’t write any custom code (apart from in Liquid templates) or depend on additional gems. Later they added support for building the website from <a href="https://github.com/features/actions">GitHub Actions</a>, allowing the use of custom code, dependencies, or even swapping out Jekyll entirely.</p>

<p>I had been keeping a list of all things I <em>could</em> do if I moved to using a custom build instead of the default Pages setup. I only wanted to move if I had reasons to actually justify doing it, rather than just complicating the deployment for no real benefit. This list was getting to a reasonable length, so I started to consider taking the plunge.</p>

<p>The thing is that <a href="https://www.youtube.com/watch?v=9qljpi5jiMQ">GitHub Actions feels bad</a>. Setting it up correctly and keeping it working just seemed like a lot of effort, whereas the previous branch-based setup had already been working for me for literally a decade. Sure there are some hiccups, but it doesn’t require any extra YAML files.</p>

<p>So instead I setup a mirror of my website on GitLab Pages. GitLab CI is much easier to setup, the most basic config can just be “use this docker image, and run this command”. You then just put <code>pages: true</code> on an action that writes to <code>public/</code> and you’re done.</p>

<p>Here’s the whole config:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>create-pages:
  image: ruby:3.4
  script:
    - gem install bundler
    - bundle install
    - bundle exec jekyll build -d public
  pages: true
  only:
    - main
</code></pre></div></div>

<p>It’s a little more complicated if you want to cache the result of <code>bundle install</code> to save time, but what I really love about this is I can see exactly what’s going to happen. If there’s some problem, I can just pull <code>ruby:3.4</code>, run a new container with my website in it, then run the commands in the <code>script</code> section.</p>

<p>Jumping ahead a little, I did end up configuring GitHub Pages, but the config is much longer, it has to configure permissions, you need a special action to actually get your code, as well as two more actions to setup and deploy to Pages.</p>

<p>Interestingly while the basic setup is simpler, GitLab doesn’t auto-compress your files like GitHub does. You have to manually create <code>.gz</code> versions of each file to have them be served with gzip compression. This is a little inconvenient, but they do support serving other compression schemes like <a href="https://brotli.org">Brotli</a>:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find public -type f -regex '.*\.\(htm\|html\|xml\|txt\|text\|js\|css\|svg\)$' -exec gzip -f -k {} \;
find public -type f -regex '.*\.\(htm\|html\|xml\|txt\|text\|js\|css\|svg\)$' -exec brotli -f -k {} \;
</code></pre></div></div>

<p>Brotli cuts 1kB off the homepage of my website (4.8kB versus 5.9kB) which is a nice improvement, so all in all I’d accept this slight increase in complexity for smaller response sizes. GitHub is limited to whatever they decide to serve, which currently is just gzip applied automatically where they see fit.</p>

<p>So I had a copy of my website on GitLab Pages working with compression and everything on an auto-generated <code>gitlab.io</code> domain. Everything worked, but you can tell just by looking at it that it’s slower to load. I’m very used to my website loading almost instantly because I pointlessly code golf down the size of the HTML and CSS to be as small as possible. I sent a link to a friend who I assume doesn’t check my site as obsessively as I do, and asked “what’s the performance like?” Their immediate response was they thought it was just a bit slower than my actual site.</p>

<p>Right now if I look at the timing in the web inspector, the increase in load time is entirely in the browser waiting for the server to start sending the actual data. Getting the SSL connection is 300ms versus 19ms, then waiting for the response is 500ms versus 8ms. Downloading the actual data from both is 0.1ms. Time to first byte is 817ms versus 32ms.</p>

<p>Doing a bit of a <code>traceroute</code> seems to indicate that GitLab is getting served from somewhere in Missouri, whereas the GitHub response headers include <code>x-served-by: cache-syd10177-SYD</code> so my request probably isn’t going further than 50km.</p>

<p>I also used the <a href="https://tools.pingdom.com">Pingdom website speed test</a> which is probably a bit more scientific than doing random requests from my laptop, and it shows a similar story: GitHub spends almost no time (13ms) establishing the SSL connection, whereas GitLab takes 700ms. Moving the request source from Sydney to San Francisco cuts this down to 300ms, so I’m definitely paying a tax for being on the wrong side of the globe.</p>

<p>It’s fun to see that all the responses are smaller because of the Brotli encoding on the GitLab version, but if you’re spending 700ms initialising the connection that doesn’t really matter.</p>

<p>Probably the only thing that would make me use GitLab Pages would be the ability to configure cache expiration. Both hosts set a default <code>Cache-Control: max-age=600</code> header to cache the response for 10 minutes, but for web fonts, CSS, and my tiny JS file, it would be great to set this to be much longer.</p>

<p>So it seemed like the best option was to stick with GitHub. I know there are plenty of other static site hosts, but I didn’t really set out to do an exhaustive comparison. Adding a different service would likely complicate my build process even more, and the whole reason I started looking at GitLab was that their CI is so much simpler than GitHub’s.</p>

<p>I rolled up my sleeves and flailed around with GitHub Actions YAML until I had a working site. I actually made a new repository so I could just commit and push over and over until it worked, then squash it all down into one perfect commit and push that to my actual repo, saving my git history.</p>

<p>The jump from Jekyll 3.10 to 4.4 (and updating all the other dependencies at the same time as well) did expose some issues.</p>

<p>Either <a href="https://rouge.jneen.net/">Rouge</a> (syntax highlighter) or <a href="https://kramdown.gettalong.org/">Kramdown</a> (markdown processor) have stopped adding a <code>highlighter-rouge</code> wrapper <code>div</code> around code blocks with no highlighting. I was using this in my CSS, and so any non-highlighted code blocks didn’t get styled correctly. This actually ended up being quite convenient, as it forced me to delete and re-write my CSS with respect to <code>&lt;pre&gt;</code> and <code>&lt;code&gt;</code>, resulting in simpler rules.</p>

<p>Kramdown also changed the HTML generated for footnote links, so they no longer have <code>role="doc-noteref"</code>. I was using this for styling and all my footnotes jumped back up to being superscripts. This was another easy fix, just change the CSS selector to be <code>sup:has(.footnote)</code>.</p>

<p>With the new Sass version, I started getting a lot of deprecation warnings for <code>lighten()</code>, <code>darken()</code>, <code>change-color()</code>, and <code>@import</code>. The colour adjustments could just be rewritten to use <code>color.change()</code> and <code>color.adjust()</code>. Correcting the imports turned out to be trickier, as I’d just split the file somewhat arbitrarily and this didn’t really fit with how Sass wanted imports to work. Instead of working out how rules, variable declarations, and functions should be separated, I just put everything into one file. Maybe I’ll work something smarter out in the future, but for now it works with no warnings.</p>

<p>So after all that I had a website that looked and worked just like it did before. Thankfully I had my list of improvements I could make, and now there was nothing to stop me.</p>

<p>The first thing I did was write a <a href="https://github.com/willhbr/willhbr.github.io/blob/76efea5f89aba57914d8f34afc0e383c5df2bd23/_plugins/markdown.rb">custom Jekyll converter that uses a custom Kramdown converter</a> to always add <code>loading="lazy"</code> to <code>&lt;img&gt;</code> tags. I’d been doing this manually by tacking <code>{:loading="lazy"}</code> onto the end of every markdown image, but now it’ll just happen by default.</p>

<p>Next I removed the liquid template that I used to get <a href="/2024/07/18/lazy-jekyll-hacks-for-more-accurate-publication-times/">more accurate publication times</a> for my latest post. Now it is just <a href="https://github.com/willhbr/willhbr.github.io/blob/76efea5f89aba57914d8f34afc0e383c5df2bd23/_plugins/filters.rb#L2">a custom Liquid filter</a>, so I just have to write <code>post.date | smart_date | date: site.date_format</code> instead of including and capturing a template.</p>

<p>The next thing is slightly cursed. In both the JSON and RSS feeds any code block has the HTML markup to be syntax highlighted, but feed readers don’t have the CSS and so they’ll always render it without styling. Including it in the feeds is a pure waste of bytes, but there’s no great way in Jekyll to render posts differently depending on where they’re being included. Instead I wrote <a href="https://github.com/willhbr/willhbr.github.io/blob/76efea5f89aba57914d8f34afc0e383c5df2bd23/_plugins/filters.rb#L11">another filter</a> that will <a href="https://stackoverflow.com/a/1732454/692410">use a regex to parse the HTML</a> and strip any <code>&lt;span&gt;</code> tags from within <code>&lt;code&gt;</code> blocks. Obviously it depends on how much code is in the post, but this cut down the size of the RSS feed by 82kB. And you know how I feel about code golfing the size of my pages.<sup id="fnref:golf-styles"><a href="#fn:golf-styles" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>A minor fix I made is the character used in the footnote backlinks—the link in the footnote text at the bottom of the post that jumps back up to where the footnote is referenced. The default character is ↩ (<code>#8617</code>, LEFTWARDS ARROW WITH HOOK), which renders differently on iOS versus MacOS. On MacOS it’s similar to other HTML arrows and renders like a letter, like this: ↩︎. On iOS it renders like a colour emoji, a blue shaded box with a white arrow in it, similar to 🆒.</p>

<p>This has always bugged me. I have no idea why there’s this inconsistency, the emoji character looks out of place. I went to see where in Kramdown I needed to make a change to get a different character, and to my surprise I found out there’s already a feature to replace the character—I could have had a different one all along. I chose to swap it to ↑, which I like more as a “jump up to where this was mentioned” link.</p>

<p>There’s actually some interesting discussion on the <a href="https://github.com/gettalong/kramdown/issues/247">feature request</a> in Kramdown. You can actually just add another code point afterwards and force it to display in the text mode, but I’m not particularly attached to that character and will stick with the up arrow.</p>

<p>Now we get to bigger changes. Up until now the way to view posts with a particular tag is to <a href="/tags/">go to the tags page</a> and scroll to find the right tag. This is <em>fine</em>, but not particularly nice. Now that I have unlimited power, I can write a <a href="https://github.com/willhbr/willhbr.github.io/blob/76efea5f89aba57914d8f34afc0e383c5df2bd23/_plugins/generator.rb">custom generator</a> that creates a new page for each tag that’s been defined. Now there’s a dedicated page for each tag which gives me a little more room to group the posts by year instead of just in one big list.</p>

<p>Probably the biggest change is adding a list of related posts to the footer of each post. Previously I just had links for the next and previous post. I don’t even know why I had those links, it must have been in the Jekyll example template or somewhere in the documentation. However since I’m not writing a series it’s not really that important to specifically go to the next or previous entry.</p>

<p>Instead I’ve <a href="https://github.com/willhbr/willhbr.github.io/blob/76efea5f89aba57914d8f34afc0e383c5df2bd23/_plugins/filters.rb#L17">written some logic in a filter</a> that gives me a certain number of relevant posts to include. Right now this will be posts that share any tags with the current post. I didn’t want to completely throw out the next/previous links, so the related posts list will always include those as well. This gives it a little more variety, and any posts without tags will still get two links at the bottom.</p>

<p>The obvious other thing to do would be to use third-party plugins or my own Ruby code to generate the RSS and JSON feeds. I’ve held off on this because while templating JSON or XML isn’t the best idea, the templates are pretty good at this point and have been working without me fiddling with them for years. Maybe if I want to add something more complicated to the feeds, but for now I think they’re fine as they are.</p>

<p>Another bit of work would be JavaScript-free footnotes. I’ve recently added a few lines of JS to make footnotes open in a popover instead of just jumping down to the bottom of the page, but it would be nice to do this with no JS at all. Now I’ve got complete control over the HTML generation, maybe there’s a better option here.</p>

<p>If you want to make the same move yourself, you can see <a href="https://github.com/willhbr/willhbr.github.io/blob/main/.github/workflows/jekyll.yml">my GitHub Actions config</a> as well as my <a href="https://github.com/willhbr/willhbr.github.io/blob/main/.gitlab-ci.yml">GitLab CI config</a> which are both currently working to publish the site live and to the GitLab mirror.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:golf-styles">
      <p>I just had a thought that I could use this same thing to strip out any <code>&lt;span&gt;</code> with a class that I don’t actually apply a style for. Actually I could include that directly in the custom markdown processor. You see, this is the kind of rabbit hole I was concerned about. <a href="#fnref:golf-styles" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2026/01/03/upgrading-to-jekyll-4-4/">Permalink</a>
&bull; January 3, 2026
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Upgrading%20to%20Jekyll%204.4">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Help I&apos;m Stuck in a Photo Management Factory and I Can&apos;t Get Out</title>
        <pubDate>Fri, 26 Dec 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/12/26/stuck-in-a-photo-management-factory/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/12/26/stuck-in-a-photo-management-factory/</guid>
        <description><![CDATA[<p><img loading="lazy" src="/images/2025/izamal.webp" alt="Photo of the convent at Izamal, México" style="aspect-ratio: auto 1200/521" /></p>

<p class="caption">I feel obliged to include a photo, since this post is about photography.</p>

<p>In 2019 I got into this whole photography business and used my iPad for photo editing in <a href="https://www.pixelmator.com/photomator/">Photomator</a>. This went pretty well until I started to shoot raw photos instead of JPEGs, and quickly ran into the meagre 250GB internal storage limit of the iPad.</p>

<p>At the time I <a href="/2019/11/02/impracticalities-of-ios-photo-management-for-photographers/">wrote about</a> how limitations of the iPad make using it as an exclusive photo-editing device impractical. Basically the only way to get photos off the iPad was to upload them to a cloud service—at the time I had a terrible internet connection, so this was basically infeasible. On paper it was possible to export to an external drive, but for any number of photos to actually make a dent in my growing library, this was unsupported.</p>

<p>The most reliable thing to do would be to copy the photos onto a “real” computer using the MacOS “image capture” utility, where you could then dump them onto an external drive or whatever you desired.</p>

<p>Eventually I decided that if I needed a real computer in order to use the iPad, I should just edit on the computer to begin with, and so I <a href="/2022/03/20/the-good-and-bad-of-photos-for-macos/">moved from the iPad to an M1 MacBook Air</a> with a nice 1T SSD.</p>

<p>In 2022 <a href="/2022/03/20/the-good-and-bad-of-photos-for-macos/">I wrote</a>:</p>

<blockquote>
  <p>My photo library currently takes up 500G and I’m not looking forward to having to split it.</p>
</blockquote>

<p>How right I was.</p>

<p>At the end of 2023 my photo library burst out of my laptop like an extra-terrestrial creature from the chest of an unsuspecting space-tug crew member. I did exactly what I had planned: I carefully duplicated, backed up, and split my photo library in two. One library with everything before 2023 would live on an external SSD, the other with all my new photos would live on the laptop’s internal storage.<sup id="fnref:how-split"><a href="#fn:how-split" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>I could continue to import and edit any new photos into the photo library on the laptop just as I had been doing. If I wanted to look at the old photos, I’d just have to plug in the SSD.</p>

<p>Except it’s not <em>quite</em> that simple. To open an alternate photo library you have to hold the option key while Photos is launching, then select it from a list (or navigate to it in Finder).</p>

<p>To actually edit something in a third-party editor (like aforementioned <a href="https://www.pixelmator.com/photomator/">Photomator</a>) you have to go into Photos’ settings and make the current library the system photo library. This is because the third-party app has no knowledge of anything <em>other</em> than a monolithic system library. You have to redirect that API to your external drive, then re-redirect it back once you’ve finished.</p>

<p>The end result was that all my photos before 2023 were lost media. Dead and inaccessible to the world.</p>

<p>So sometime last year I moved <em>everything</em> onto a bigger 2T SSD<sup id="fnref:ssd-brand"><a href="#fn:ssd-brand" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>. This required whole extra process where I merged the two libraries back into one using <a href="https://www.fatcatsoftware.com/powerphotos/">PowerPhotos</a>. Multiple backups, merged, checked. Finally I had everything in one place with a path forward: the 2T SSD left me 700GB of headroom, and if I filled that I could just go and get a 4T or 8T drive and copy everything across.</p>

<p>This even works better than just buying a new laptop with more internal storage, since the MacBook Air doesn’t (currently) come with more than 2T of storage. So even if I’d spent the extra $600 to go from 1T to 2T, going to 4T means getting a bulky MacBook Pro.<sup id="fnref:macbook-pro-costs"><a href="#fn:macbook-pro-costs" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<p>It seemed like a great solution. It’s even a <a href="https://support.apple.com/en-us/108345">supported way to use the Photos app</a> and <a href="https://sixcolors.com/post/2025/08/work-around-icloud-photos-optimized-limitations/">mentioned by internet-resident Photos experts</a>.</p>

<p>What this documentation doesn’t mention is that the system expects the library to always be available. The processes that slowly dawdle through your library identifying faces and whatnot will keep the database open, so the drive basically can’t be safely ejected.</p>

<p>Of course you can just rip the cable out or click “force eject” and gamble with data integrity every time. Plenty of people seem to think that ejecting or unmounting is a thing of the past, from the era of floppy disks and CD drives.</p>

<p>Most of the time when I want to unplug the drive I’d have to resort to force ejecting it, since you can’t stop the Photos system from retaining its access to the library. I don’t know if this is what caused it, but fairly often Photos would open the library and have to spend a few minutes “repairing” it before you could do anything.</p>

<p>Other times it would completely fail to open the library with absolutely no recourse, just an error message saying “the library could not be opened”. Through completely dumb luck I worked out that opening PowerPhotos would kick Photos back into gear and it would load the library.</p>

<p>While I haven’t actually got into an irrecoverable state yet, it’s disconcerting seeing your carefully organised photo collection fail to load every so often. Maybe a future OS update will fix whatever causes it to get into this state—but maybe an update will stop PowerPhotos’ ability to kick it back into shape?</p>

<p>Then even after all this trouble, that’s just the photo library. The edits from Photomator (and Pixelmator Pro) are saved in “sidecar” files in <code>~/Pictures/Linked Files</code> (this has moved around a little). There isn’t a supported way of storing these on the external drive, I have tried to use symbolic links to keep all the files for Pixelmator Pro and Photomator together in one folder, but at least when I last tried one or both of them wouldn’t follow the link and would just fail to save the file.</p>

<p>So here I am, 1.3T of photos sitting on an external drive in a photo library that <em>might</em> be corrupted any time I go to use it.</p>

<p>It won’t come as a surprise that since I filled up my internal drive, the number of times I’ve gone out to take photos has dropped off dramatically. Some of this is me <a href="/2025/12/13/programming-with-tmux-for-beginners/">spending my time on other interests</a>, but a large part is the sense of dread I get knowing that every photo I take is just digging me further into a pit of data management hell that I have no way out of.</p>

<p>I don’t know what the solution is, no one seems to <em>enjoy</em> using Lightroom. <a href="https://www.affinity.studio/">Affinity</a> Photo has been merged into one mega-app, but it didn’t have photo library management in the first place anyway.</p>

<p>There are interesting other options like <a href="https://aspect.bildhuus.com">Aspect</a>, but it’s only organisation software—no editing. They say it’ll recognise “popular” editors, but the exact details of that could make or break the workflow for me. I really value being able to flick between photos and go from viewing to editing with minimal faff. This was the main <a href="/2023/05/24/photomator-for-macos/">reason for me to buy Photomator</a> even though it doesn’t offer different editing capabilities from Pixelmator Pro.</p>

<p>Even if I did find another system, it would likely require a substantial (stressful) migration of my existing library. Would a move just be digging further into the hole, or would it actually get me onto a more sustainable path?</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:how-split">
      <p>Honestly this was such a process it could be a post of its own. <a href="#fnref:how-split" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:ssd-brand">
      <p>A Samsung T7 drive. I’ve got that and a T5 and they seem good. I use SanDisk MicroSD cards but am <a href="https://www.theverge.com/22291828/sandisk-extreme-pro-portable-my-passport-failure-continued">scared of their SSDs failing</a>. <a href="#fnref:ssd-brand" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:macbook-pro-costs">
      <p>Going from a 2T MacBook Air to the cheapest MacBook Pro with 4T costs an extra $2000. Getting 8T costs $3200 on top of that. <a href="#fnref:macbook-pro-costs" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/12/26/stuck-in-a-photo-management-factory/">Permalink</a>
&bull; December 26, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Help%20I'm%20Stuck%20in%20a%20Photo%20Management%20Factory%20and%20I%20Can't%20Get%20Out">email</a>.
]]></description>
      </item>
    
      <item>
        <title>My Website Broke and You Won&apos;t Believe Why</title>
        <pubDate>Wed, 24 Dec 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/12/24/my-website-broke-and-you-wont-believe-why/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/12/24/my-website-broke-and-you-wont-believe-why/</guid>
        <description><![CDATA[<p>When I published <a href="/2025/12/14/add-a-signature-to-your-website/">my last post</a> I did my usual quick check on the real website, just to make sure it had published, and find the obligatory mistakes that only appear once it’s public. I quickly noticed that the XML code block didn’t have any syntax highlighting, just a plain unstyled <code>&lt;code&gt;</code> section.</p>

<p>My site uses <a href="https://rouge.jneen.net/">Rouge</a> for syntax highlighting, which I think is the default for GitHub Pages sites that are built with the (now legacy) non-actions system. I’ve never included an XML code block so <em>maybe</em> Rouge doesn’t support it, but it supports so many languages it would be a significant omission.</p>

<p>It’s weird that I hadn’t noticed this while writing when I was running the site locally, and sure enough I hadn’t noticed it because the local site was working exactly as I expected with <a href="/2024/10/03/web-gardening/#syntax-highlighting">beautiful hand-crafted syntax highlighting</a>.</p>

<p>So it works locally, but doesn’t work on the live site.</p>

<p>I run the site locally in a container and install all the dependencies through the <a href="https://github.com/github/pages-gem"><code>github-pages</code></a> gem, which should track the exact version of Jekyll and of all the available plugins, so my local version should be exactly the same as the live one.</p>

<p>Inspecting the actual HTML of the local site versus the live site, there’s a pretty obvious difference. Here’s the markup for the local site (truncated):</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;div class="language-xml highlighter-rouge"&gt;
  &lt;div class="highlight"&gt;
    &lt;pre class="highlight"&gt;
      &lt;code&gt;
        &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;
        &lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt;
        &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1364"&lt;/span&gt;
        &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"486"&lt;/span&gt;
        &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 1364 486"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;
        &lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#000"&lt;/span&gt;
        &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;
        ...
      &lt;/code&gt;
    &lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre></div></div>

<p>Then here’s the markup I was seeing on the live site:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;div class="language-xml highlighter-rouge"&gt;
  &lt;div class="highlight"&gt;
    &lt;pre class="highlight language-xml" tabindex="0"&gt;
      &lt;code class="language-xml"&gt;
        &lt;span class="token tag"&gt;&lt;span class="token tag"&gt;&lt;span class="token punctuation"&gt;&amp;lt;&lt;/span&gt;svg&lt;/span&gt;
          &lt;span class="token attr-name"&gt;xmlns&lt;/span&gt;&lt;span class="token attr-value"&gt;
            &lt;span class="token punctuation attr-equals"&gt;=&lt;/span&gt;
            &lt;span class="token punctuation"&gt;"&lt;/span&gt;http://www.w3.org/2000/svg&lt;span class="token punctuation"&gt;"&lt;/span&gt;
          &lt;/span&gt;
          &lt;span class="token attr-name"&gt;width&lt;/span&gt;&lt;span class="token attr-value"&gt;
            &lt;span class="token punctuation attr-equals"&gt;=&lt;/span&gt;
            &lt;span class="token punctuation"&gt;"&lt;/span&gt;1364&lt;span class="token punctuation"&gt;"&lt;/span&gt;
          &lt;/span&gt;
          &lt;span class="token attr-name"&gt;height&lt;/span&gt;&lt;span class="token attr-value"&gt;
            &lt;span class="token punctuation attr-equals"&gt;=&lt;/span&gt;
            &lt;span class="token punctuation"&gt;"&lt;/span&gt;486&lt;span class="token punctuation"&gt;"&lt;/span&gt;
          &lt;/span&gt;
          ...
      &lt;/code&gt;
    &lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre></div></div>

<p>The exceptionally short class names (<code>na</code>, <code>s</code>, <code>nt</code> and such) are what I expect to get from Rouge, but instead I was getting much more detailed, longer class names that didn’t match my CSS, so they were left as plain un-highlighted text.</p>

<p>Maybe GitHub is rolling out a new version of the Pages gem that includes a newer Rouge version that creates incompatible markup? That would be pretty rude and also unlikely, there’s nothing in the Rouge changelog that would indicate a breaking change like this.</p>

<p>Or maybe they’ve updated something and Jekyll no longer respects the <code>highlighter: rouge</code> configuration option, so it’s falling back to some other highlighter that creates different markup. That would also be a rude change, and there hasn’t been a release of the Pages gem since <a href="https://github.com/github/pages-gem/releases/tag/v232">August 2024</a>. It seems unlikely that the gem would be out of sync with the system that actually builds your website.</p>

<p>A bit stumped, I asked a friend if they saw the highlighting and they said they did. So it’s just a me problem. I tried in Firefox and had no issue—the highlighting showed up exactly as expected.</p>

<p>You might be thinking “it’s obvious Will, you’ve got some browser extension that’s messing it up!” But no, I’ve only got two extensions: <a href="https://1blocker.com">1Blocker</a> and <a href="https://1password.com">1Password</a>.<sup id="fnref:extension-naming"><a href="#fn:extension-naming" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> There’s no way either of these would alter the syntax highlighting in code blocks.</p>

<p>1Blocker mostly (as far as I’m aware) uses the <em>Content Blocker</em> API that just hides elements in the DOM, rather than mutating them.</p>

<p>1Password should only be adding a little account selection dropdown on pages with a login form. There’s absolutely no reason for their extension to do anything on my website apart from go “nope no login form here”. <sup id="fnref:no-api"><a href="#fn:no-api" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>Well out of complete desperation as I didn’t have any better ideas, I disabled both, and sure enough the highlighting worked.</p>

<p>It turns out 1Password is applying its own syntax highlighting to any block matching this selector:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code[class*="language-"], [class*="language-"] code,
code[class*="lang-"], [class*="lang-"] code
</code></pre></div></div>

<p>Searching in the extension code for “token” (you can view the source of all scripts injected by extensions in the developer tools) quickly led me to the unobfuscated <code>highlightAllUnder</code> function name, with that selector. A post on the <a href="https://www.1password.community/discussions/developers/1password-chrome-extension-is-incorrectly-manipulating--blocks/165639">1Password forum</a> identifies this code as coming from <a href="https://prismjs.com/">prism.js</a>, a JavaScript code highlighting library.</p>

<p>So there you go, all my wondering about caching and GitHub Pages gem versions was for nothing. I was blinded by the fact it’s absurd that my password manager is injecting a code highlighting script into every page I visit, so I didn’t bother to try disabling extensions sooner.</p>

<p>I contacted 1Password support and they confirmed what I’d already found in the forum; it’s a known issue and they’re working on a fix. Hopefully they will share some information on how this code got into the extension. My assumption is that it’s used in the main app for some feature (like code blocks in notes) and accidentally got included as a dependency of the browser extension.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:extension-naming">
      <p>Seemingly I only use 1Extension. <a href="#fnref:extension-naming" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:no-api">
      <p>The fact that 1Password requires a browser extension instead of using the OS’s specifically-designed API for autofilling passwords is not great. It was understandable before the widespread availability of system-level APIs for autofill, but <a href="https://developer.apple.com/documentation/AuthenticationServices">iOS and MacOS have had these APIs</a> for over 7 years now. They support it on iOS, but MacOS is left with a misbehaving browser extension. <a href="#fnref:no-api" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/12/24/my-website-broke-and-you-wont-believe-why/">Permalink</a>
&bull; December 24, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20My%20Website%20Broke%20and%20You%20Won't%20Believe%20Why">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Add a Signature to Your Website</title>
        <pubDate>Sun, 14 Dec 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/12/14/add-a-signature-to-your-website/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/12/14/add-a-signature-to-your-website/</guid>
        <description><![CDATA[<p>Nothing says “sophisticated and refined” like putting your signature at the bottom of your blog posts. However if you don’t do it right, you might end up with pixellated garbage, and that’s not refined at all. I’ve just done this (have a look at the bottom of this post) and there are a few tricks to get this working nicely.</p>

<p>First thing is to get a signature. I didn’t want to use the unintelligable scrawl I leave on important documents, but instead put a little more emphasis on my domain name—it’s my name and initials.</p>

<p>I used my iPad and <a href="https://www.goodnotes.com">GoodNotes</a> to sign over and over again until I had one I was happy with. You could of course do this with pen and paper, the key is to just use a writing implement that leaves a stroke with uniform width and darkness. GoodNotes is great for this because it has my favourite pen mode: “the best fine-point felt-tip pen you’ve ever used”. We’re going to be converting this to an SVG later, and my SVG-ing skills can’t handle variable width strokes, so the uniform width is key.</p>

<p>There’s probably some way to get an SVG out of GoodNotes, but like so many things on iPadOS you’ll fight the implicit conversion to an raster image at some point in the process anyway. I just selected the good signature and copied it to the <a href="https://support.apple.com/en-us/102430">universal clipboard</a> so I could paste it into <a href="https://www.pixelmator.com/pro/">Pixelmator Pro</a> on my laptop.</p>

<p>At this point you could just save the signature as a PNG, slap it on your website, and call it a day. But we can do better.</p>

<p>There’s probably some nice tool to convert an image to clean vector shapes, but I opted to just trace it manually. I used the “freeform pen” tool in Pixelmator, this is a standard tool in any image editor with vector features, it lets you draw a shape freehand and have the result be a bezier curve.</p>

<p>I traced over each stroke in the signature really badly using the trackpad. It would’ve been easier with a mouse but I didn’t want to get off the couch. The quality of the first pass isn’t really important, as you’ve got to go over each stroke and use the vector control nubs to get it as close as possible to the original signature. Here you could have a little artistic license and smooth out some curves or other imperfections.</p>

<p>Now we’ve got an SVG, which we could totally just put on our website and appreciate the infinite scalability, but it’s not quite that simple.</p>

<p>The first trap is that if you export as SVG from Pixelmator Pro it will do the completely reasonable thing of including all your raster layers as <code>&lt;image&gt;</code> tags with base64 encoded data (even if they’re hidden). My exported SVG was 137kB, which is huge.</p>

<p>You could delete them manually but there’s a better way: <a href="https://svgo.dev">SVGO</a>. It’s an SVG optimiser, which shrinks the size of SVGs by combining paths and removing unnecessary data. I have no idea how it works, it seems like magic to me. This is the original SVG (with data omitted for readability):</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;!-- Generated by Pixelmator Pro 3.7.1 --&gt;
&lt;svg width="1364" height="486" viewBox="0 0 1364 486"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
    &lt;path id="Path" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path1" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path2" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path3" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path4" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path5" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path6" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path7" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path8" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path9" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;path id="path10" fill="none" stroke="#000"
      stroke-width="25" stroke-linecap="round"
      stroke-linejoin="round" d="..."/&gt;
    &lt;image id="Layer" x="2" y="-1" width="1364"
      height="490" visibility="hidden"
      xlink:href="data:image/png;base64, ..."/&gt;
&lt;/svg&gt;
</code></pre></div></div>

<p>Then here’s the optimised SVG (again with data omitted):</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;svg xmlns="http://www.w3.org/2000/svg" width="1364"
  height="486" viewBox="0 0 1364 486"&gt;
  &lt;path fill="none" stroke="#000" stroke-linecap="round"
    stroke-linejoin="round" stroke-width="25" d="..."/&gt;
  &lt;path fill="none" stroke="#000" stroke-linecap="round"
    stroke-linejoin="round" stroke-width="25" d="..."/&gt;
  &lt;path fill="none" stroke="#000" stroke-linecap="round"
    stroke-linejoin="round" stroke-width="25" d="..."/&gt;
  &lt;path fill="none" stroke="#000" stroke-linecap="round"
    stroke-linejoin="round" stroke-width="25" d="..."/&gt;
&lt;/svg&gt;
</code></pre></div></div>

<p>It removed the invisible image (I would have done that manually anyway) but it also decided I only needed four <code>&lt;path&gt;</code> tags, removed the comment, and removed a <em>tonne</em> of point data from the actual paths. The original file was 137kB, removing the <code>&lt;image&gt;</code> takes that to 4.6kB (totally reasonable SVG size), then SVGO cuts that in half to 2.4kB. With no difference that I can see.</p>

<p>We can then put the SVG on our website and marvel at the small file size and resolution-independence. However, there’s a tradeoff: if we embed the SVG directly into the page with an <code>&lt;svg&gt;</code> tag, it can be styled by the site’s CSS rules and thus respond to light and dark mode, but that will increase the size of every HTML page by 2.4kB. If it’s included with an <code>&lt;img&gt;</code> tag it’ll be loaded once and cached, but it can’t access styles.</p>

<p>My site has both light and dark themes, so the signature needs to respond to the CSS, but I also don’t want a 2.4kB increase on every page.<sup id="fnref:dont-ask"><a href="#fn:dont-ask" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> Thankfully we can get the best of both worlds.</p>

<p>The trick is to use the SVG as an <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/mask-image"><code>mask-image</code></a> for an element that has the <code>background-color</code> we want the stroke of our SVG to be. This only works for monochrome images (without much more complicated trickery) which is perfect for this use case. On the page we just need a placeholder element:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;div id="signature"&gt;&lt;/div&gt;
</code></pre></div></div>

<p>Then in CSS (or in my case, SASS) we set the <code>mask-image</code> and some other <code>mask-</code> properties to ensure we only get a single, correctly-sized signature.</p>

<div class="language-sass highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#signature
  mask-image: url("/images/signature.svg")
  mask-size: contain
  mask-repeat: no-repeat
  mask-position: center
  background: $text-colour
  height: 3em
</code></pre></div></div>

<p>The <code>&lt;div&gt;</code> placeholder will change colour between light mode and dark mode, only visible through the <code>mask-image</code>, giving the impression that the SVG is able to change colour, without wasting 2.4kB on every page.</p>

<p>Related, I really like this <a href="https://www.joshwcomeau.com/svg/friendly-introduction-to-svg/">introduction to SVG</a> which really shows off how much more you can do with it. That’s what made me choose SVG for the graph in <a href="/2025/10/20/light-mode-infffffflation/"><em>Light Mode InFFFFFFlation</em></a>.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:dont-ask">
      <p>Don’t ask why I insist on minimising the size of my site but I’m happy serving four web fonts. <a href="#fnref:dont-ask" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/12/14/add-a-signature-to-your-website/">Permalink</a>
&bull; December 14, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Add%20a%20Signature%20to%20Your%20Website">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Programming With tmux for Beginners</title>
        <pubDate>Sat, 13 Dec 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/12/13/programming-with-tmux-for-beginners/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/12/13/programming-with-tmux-for-beginners/</guid>
        <description><![CDATA[<p>Last week I gave a short talk on my adventures doing weird things with tmux. It went well, so I decided to record the slides with voiceover:</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" width="650" height="400" src="https://www.youtube.com/embed/5Uh2mYl95Z8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>
</div>

<p>It’s <em>very</em> high level, the code snippets are simplified for readability, and I don’t go into much detail on the process of working this all out, but I think it’s a nice introduction to the whole project.</p>

<p>Of course you can read more about:</p>

<ul>
  <li><a href="/2024/03/15/making-a-compiler-to-prove-tmux-is-turing-complete/">Writing the Brainfuck and Python compilers</a></li>
  <li><a href="/2024/03/16/further-adventures-in-tmux-code-evaluation/">Improving the process with key bindings</a></li>
  <li><a href="/2024/12/27/solving-sudoku-with-tmux/">The sudoku solver that I didn’t have enough time to go into</a></li>
  <li><a href="/2025/03/17/playing-video-with-5170-tmux-windows/">Playing video in tmux</a></li>
  <li><a href="/2025/03/20/snakes-in-a-pane/">Implementing Snake</a></li>
</ul>

<p>Then the things I didn’t mention are <a href="/2025/01/10/how-did-i-miss-run-c/">keeping things inside tmux with <code>run -C</code></a> and <a href="/2025/10/25/full-speed-tmux-interpreter/">unlocking the full speed of tmux</a>.</p>

<p>You can watch the talk in the video above, or <a href="https://www.youtube.com/watch?v=5Uh2mYl95Z8">on YouTube</a>.</p>


<a href="https://willhbr.net/2025/12/13/programming-with-tmux-for-beginners/">Permalink</a>
&bull; December 13, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Programming%20With%20tmux%20for%20Beginners">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Why Do Containers on Alpine Forget My Tailnet?</title>
        <pubDate>Sun, 30 Nov 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/11/30/alpine-containers-forget-tailnet/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/11/30/alpine-containers-forget-tailnet/</guid>
        <description><![CDATA[<p>Earlier this year I wrote about <a href="/2025/03/09/a-slim-home-server-with-alpine-linux/">how I’d swapped my home “production” server</a> over to use <a href="http://alpinelinux.org">Alpine Linux</a>. Overall it’s gone well, I swapped the VPS that runs <a href="https://topomap.willhbr.net">NZ Topomap of the Day</a> (<a href="/2025/03/28/building-nz-topomap-of-the-day/">read more about that</a>) to Alpine in June, and my <a href="https://codeberg.org/willhbr/alpine-podman-setup">script that sets up an Alpine install</a> has made this straightforward.</p>

<p>This weekend I swapped the hardware that I was using for the home “production” server to be a little more recent and reliable, and was reminded of a wrinkle in Alpine that I’d run into before. Thankfully I’d written down some notes that helped me solve the issue again. I can highly recommend keeping notes on random problems you’ve seen or solved, it’s saved me loads of time trying to find the right docs again. Or even better, writing it in a blog post so other people can solve the same problem.</p>

<p>Anyway.</p>

<p>The issue I was seeing was that some containers that need to talk to other devices on my <a href="http://tailscale.com">Tailnet</a> (ie Tailscale network) would just lose the ability to resolve their addresses after a while. Frustratingly this wasn’t consistent, it would be working but then a while later it would stop.</p>

<p>I use Tailscale’s <a href="https://tailscale.com/kb/1081/magicdns">MagicDNS</a> feature which allows you to refer to any device by its hostname instead of the fully-qualified name or IP on the Tailnet. The culprit containers were <a href="https://codeberg.org/willhbr/endash">endash</a> (my container dashboard) and <a href="http://prometheus.io">Prometheus</a>, both of which connect to other devices on the Tailnet by hostname.</p>

<p>What I learnt is that the MagicDNS feature isn’t actually magic, it’s just setting a <a href="https://en.wikipedia.org/wiki/Search_domain">search domain</a> for the unique Tailnet domain name (like <code>my-tailnet.ts.net</code>) and running a custom DNS server that resolves these to the Tailnet IP addresses.</p>

<p>This works by setting some config in <code>/etc/resolv.conf</code> that looks like this:</p>

<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nameserver 100.100.100.100
search my-tailnet.ts.net
</code></pre></div></div>

<p class="caption">It’s not magic, it’s just a config file.</p>

<p>When you create a container, there are a bunch of flags you can pass (<a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#dns-ipaddr">like <code>--dns</code></a>) that override the <code>resolv.conf</code> file. If you don’t provide any of these options, Podman will use the host DNS configuration from the host’s <code>resolv.conf</code> file.</p>

<p>Where the problem comes in is that some process in Alpine will fight Tailscale and write its own <code>resolv.conf</code>, removing the MagicDNS config. Tailscale might rewrite the file, but the config is copied into the container on create and so any containers created while the incorrect config was present will continue to be broken. Updating the file and restarting the container isn’t even enough to fix it—you need to delete and recreate the container, since the config is part of its overlay filesystem.</p>

<p>I wish I had a wonderful solution that made all the pieces play nicely together, but instead you can just tell Alpine to please not overwrite the file. In <code>/etc/udhcpc/udhcpc.conf</code>, ensure that this section is uncommented:</p>

<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Do not overwrite /etc/resolv.conf
RESOLV_CONF="no"
</code></pre></div></div>

<p>Then make sure <code>/etc/resolv.conf</code> is in the state you expect (with <code>search</code> for your tailnet and the Tailscale <code>nameserver</code>), then delete and recreate any containers that need this DNS config.</p>


<a href="https://willhbr.net/2025/11/30/alpine-containers-forget-tailnet/">Permalink</a>
&bull; November 30, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Why%20Do%20Containers%20on%20Alpine%20Forget%20My%20Tailnet?">email</a>.
]]></description>
      </item>
    
      <item>
        <title>More Commands in the JJ Toolbox</title>
        <pubDate>Sat, 22 Nov 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/11/22/more-commands-in-the-jj-toolbox/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/11/22/more-commands-in-the-jj-toolbox/</guid>
        <description><![CDATA[<p>It’s been almost two years since I started using JJ regularly, and almost 18 months since I <a href="/2024/05/26/some-hot-jj-tips/">wrote some tips on how to use it</a>. That post was really just the result of me reading the docs (which at the time were much sparser than they are now) and working out how to manage remotes properly.</p>

<p>That was a long time ago, and I’ve had more time to settle into a rhythm and realise what works for me and what doesn’t.</p>

<p>Before I get too carried away, I want to get up on my high horse for a second. I found a repo that boasted “over 20 aliases for efficient workflows” and I just want to say: no. You don’t need lots of aliases. Aliases that you don’t know are useless. Having to remember which letter salad corresponds to the exact combination of flags you need is not saving you time.</p>

<p>Seriously, the oh-my-zsh git plugin <a href="https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/git/README.md">defines over 200 aliases</a>. Something has gone terribly wrong.</p>

<p>I have seven VCS-related shell aliases, which is the amount of letter salad that I can handle.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alias g="jj"
alias gs=" g status"
alias gd=" g diff"
alias gcm=" g commit -m"
alias gc=" g commit"
alias gl=" g log"
alias gp=" g push"
</code></pre></div></div>

<p>Anyway this post isn’t supposed to just be <a href="/2024/04/01/its-not-me-its-git/">complaining about git</a>. We’re here for tips.</p>

<h1 id="grab-other-versions-of-files-with-restore">Grab other versions of files with <code>restore</code></h1>

<p>The command I’m surprised by my usage of is <code>restore</code>. Since this is such a messy concept in git (are you discarding untracked, unstaged, or staged changes?) I wasn’t in the habit of doing this. The only command I knew was <code>git checkout -- .</code> which would blow away any tracked changes and get you to an empty working copy. It’s not a very precise operation.</p>

<p>I’ve got basically three different usages of <code>restore</code>. The first is when I’ve got a change but it contains some debugging code or something that I don’t want to be included when I send it out for review. I’ll use <code>jj restore -i</code> to show the interactive diff editor and select the bits I want to get rid of.</p>

<p>If I’ve got a commit and I’m working on top of it, sometimes I want to drop a file back to its state on the main branch. I could just rebase the commit I’m working on, but <code>restore</code> makes it easy to get a file to the state it was in on a different revision, usually <code>main</code>. I’ll do this with <code>jj restore -f main path/to/my/file.txt</code> and now my working copy has the updated file.</p>

<p>If you think about it, <code>jj duplicate</code> is just <code>jj restore</code> with all files into an existing empty commit.</p>

<p>The last use is the predictable one, if I’ve made some change and it’s just plain bad, I’ll do <code>jj restore</code> with no extra arguments to simply discard my changes. This is equivalent to <code>jj abandon</code> but feels a little safer.</p>

<p>Of course that safety doesn’t really matter, since I can <code>jj undo</code> anything anyway. This has been surprisingly handy if I get myself into a state with lots of merge conflicts, or accidentally run a command with the wrong flags. It just removes the risk associated with making a mistake, which means I don’t have to be particularly confident that any one command will do exactly what I expect. If it doesn’t, I’ll just undo and check the docs.</p>

<h1 id="irresponsibly-juggle-revisions-with-rebase">Irresponsibly juggle revisions with <code>rebase</code></h1>

<p>I did define an alias <code>onmain</code> that would move the working copy to be based on <code>trunk()</code> instead of wherever it is currently. It’s fine, it works, but to be honest it’s easier to just do <code>rebase -d main</code>.</p>

<p>Initially I think I got a bit confused with the <code>-r</code> flag to <code>rebase</code>, but once I realised <code>-s</code> (or <code>--source</code>) and <code>-d</code> (or <code>--destination</code>) do exactly what you want, I’ve had no trouble.</p>

<p>You can get a little fancy with <code>-A</code> and <code>-B</code> (<code>--insert-after</code> and <code>--insert-before</code>) which lets you splice a change right in-between two others, but this is a bit too much for me to remember. I’ll just run <code>rebase</code> twice.</p>

<h1 id="move-changes-between-revisions-with-squash">Move changes between revisions with <code>squash</code></h1>

<p>Something I thought I’d miss in JJ is the lack of an equivalent to <a href="https://wiki.mercurial-scm.org/HisteditExtension"><code>hg histedit</code></a>. This opens a nice TUI that works similarly to an interactive rebase in git. You can choose for each commit whether you want to fold or edit or whatever, and then you say “go” and it does it all.</p>

<p>I’d use this to reorder commits (so one change could get submitted before another) but often all I would do was make a dummy commit, then reorder it to be on top of a commit further down in the history, then fold them together. This is just a really roundabout way of doing <code>squash</code>. So instead of all that nonsense, I’ll just run <code>jj squash -d xyz</code> and the working copy changes will be moved into commit <code>xyz</code>. If I don’t want to move all the files, I’ll use <code>-i</code> to select them interactively. I find the interactive selection easier than passing file paths as arguments most of the time.</p>

<p>In Mercurial I’d use <code>hg absorb</code> for this same job, which is still present as <code>jj absorb</code>. However, neither match up the edits to the right commit every time, so using <code>jj squash</code> is more predictable.</p>

<p>It’s worth using a little bit of your brain space to learn what the “default” arguments are to various JJ commands. For example with <code>squash</code> if you give it no arguments it takes all the changes from the current commit and moves them to the parent. If you provide a revision with <code>-r</code> then it’ll move the changes from that to its parent. If you provide <code>-f</code> it’ll squash from that revision into the current one, if you provide <code>-t</code> then it’ll squash from the current into that. Other commands like <code>rebase</code> and <code>restore</code> have similar behaviour.</p>

<p>Of course it’s not difficult to just always pass <code>-f</code> and <code>-t</code>, but once you get a little fancy you can throw in some revset expressions (like <code>xyz::</code> to get all descendants) and do some clever nonsense.</p>

<h1 id="doing-fancy-revset-expressions">Doing fancy revset expressions</h1>

<p>Speaking of revset expressions, since I spent a bit of <a href="/2024/08/18/understanding-revsets-for-a-better-jj-log-output/">time learning the syntax</a> I’ll find occasions to use a revset to replace a set of tedious commands with a single command.</p>

<p>I wrote a script to make automated changes to a codebase, and it would do <code>jj new</code> before making any changes. For some files it would make no changes and I’d be left with an empty commit. There were two ways that I ended up solving this, I could get rid of all the empty commits with <code>jj abandon 'empty() &amp; mutable()'</code>, or I could merge everything back into one commit with <code>jj squash -f 'mutable()' -t @</code> (remembering that I could totally omit that <code>-t @</code> and leave it implied).</p>

<p>Obviously most of the time it’s easier to just write the revision ID, use a simple expression like <code>@-</code>, or a branch name like <code>main</code>, but it’s nice having this in your repertoire for scripting or one-off weirdness.</p>

<p>In a way this is similar to Vim commands; you can get away with super basic editing and movement commands, but if you can remember a few tricks like <code>diw</code> or <code>ci{</code> you’ll be able to get things done more smoothly.</p>

<h1 id="scripting-with-the-power-of--t">Scripting with the power of <code>-T</code></h1>

<p>Originally—for some reason—I thought I’d leave scripts using git. I have no idea why I thought this, scripting with JJ is so much easier. I find the documentation a little confusing, but almost every command accepts a <code>-T</code> or <code>--template</code> flag that dictates how the output is formatted. It is then easy to write a command that outputs just the fields you need in JSON that is trivial to parse in almost any language. This is what I did when I wrote (and then re-wrote) my <a href="/2025/04/26/writing-in-crystal-rewriting-in-rust/">project progress printer</a>.</p>

<p>The simpler model also makes scripting easier as you don’t have to worry about the working copy state, or things like where you’re going to <code>git pull</code> from. I just run <code>jj sync</code> (aliased to <code>jj git fetch --all-remotes</code>) and the repo is updated.</p>

<p>An alias that makes a lot of scripts easier is my <code>jj ls</code> alias, which lists the files touched by a particular change:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ls = ['log', '--no-graph', '-T', 'diff.files().map(|f| f.target().path()).join("\n") ++ "\n"']
</code></pre></div></div>

<p>This makes use of the template to process the list of changes files into a list of paths and then join them into a string. Embedded little languages in tools is really useful.</p>

<h1 id="the-aliases-i-do-have">The aliases I do have</h1>

<p>I really came out swinging at the start, but I do actually have some handy aliases that make life easier:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>clone = ['git', 'clone']
ig = ['git', 'init', '--git-repo=.']
sync = ['git', 'fetch', '--all-remotes']
</code></pre></div></div>

<p>I think if you’re typing any <code>git</code> subcommand with any regularity, you should alias that away. The only one I use is <code>jj git remote</code>, but that’s quite rare.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>evolve = ['rebase', '--skip-emptied', '-d', 'trunk()']
pullup = ['evolve', '-s', 'immutable()+ ~ immutable()']
</code></pre></div></div>

<p>Both of these are to update commits to sit on top of a newly-synced main branch. <code>evolve</code> works for the currently checked out branch, but I got frustrated at having to do this multiple times if I had multiple parallel changes. For that I made <code>pullup</code> (named since it pulls the changes from below <code>trunk()</code> to be on top of <code>trunk()</code>). The revset could probably be tidier, I don’t know why I didn’t just use <code>mutable()</code>.</p>

<hr />

<p>I know I poke fun at people that say they only use six git commands, but the more I think about it the more I realise I am slowly enlightening myself to realise that all these different JJ commands actually do the same thing. This time it’s different because these six commands are good.</p>

<p>Anyway this ended up more of a ramble than I expected. You can see my actual <a href="https://codeberg.org/willhbr/dotfiles/src/branch/main/jj/jjconfig.toml">JJ config on Codeberg</a> and maybe when you’re reading this I’m using 200 aliases and have reached new heights of productivity.</p>


<a href="https://willhbr.net/2025/11/22/more-commands-in-the-jj-toolbox/">Permalink</a>
&bull; November 22, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20More%20Commands%20in%20the%20JJ%20Toolbox">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Hot ECR Reloading in Your Area</title>
        <pubDate>Mon, 17 Nov 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/11/17/hot-ecr-reloading-in-your-area/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/11/17/hot-ecr-reloading-in-your-area/</guid>
        <description><![CDATA[<p>Everyone knows that ECR—the templating system built into the Crystal standard library—works at compile time, which makes it as efficient as writing to an <code>IO</code> manually. This is unlike other templating formats (usually in interpreted languages) like ERB (embedded Ruby) that parse and evaluate the template at runtime. This has the advantage of being able to change the template contents without stopping and restarting the program.</p>

<p>After I realised that ECR is <a href="/2025/11/13/html-safe-ecr-templates/">just a few classes in the standard library</a> that are actually very easy to modify, I realised that I could get a lot of the runtime reloading advantages of ERB in ECR with some reasonably horrific hacks.</p>

<p>Firstly, I just want you to understand just how cool ECR is. I always assumed it was much more complicated than it actually is, I thought it did a full parse and had to understand the Crystal code within the tags, but it’s actually much cleverer than that.</p>

<p>There isn’t even a parser, there is <a href="https://github.com/crystal-lang/crystal/blob/1c72aa8f20d40bfdfed1324df35bb33419b774e1/src/ecr/lexer.cr">a lexer</a> and that goes straight into the code generator. No messing about.</p>

<p>What happens is the lexer trundles along until it comes across an opening tag (either <code>&lt;%</code>, <code>&lt;%=</code>, or <code>&lt;%-</code>). All the text before the tag is a single string literal token. It keeps looking at the code inside the tag until it gets to a closing tag (<code>%&gt;</code> or <code>-%&gt;</code>) and then the whole section of code is one single token. It keeps going like this until it gets to the end of the file.</p>

<p>The real magic happens in the code generator. The contents of the code blocks are effectively just dumped unmodified into the output, so if we have this ECR:</p>

<pre><code>ECR solves at least &lt;%= 1 &lt;&lt; 10 %&gt; problems
</code></pre>

<p>We get this code:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>io &lt;&lt; "ECR solves at least"
(1 &lt;&lt; 10).to_s io
io &lt;&lt; " problems"
</code></pre></div></div>

<p>In this case the code section is just a single expression, so it’s fairly straightforward. Surely though if we have control flow, or a block, we’d have to do something different? No! It just follows the same formula:</p>

<pre><code>&lt;% 10.times do |i| %&gt;
 line number &lt;%= i %&gt;
&lt;% end %&gt;
</code></pre>

<p>Since Crystal doesn’t rely on significant whitespace or anything, we can just pop the contents of each of those code blocks into the generated file:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10.times do |i|
io &lt;&lt; "line number "
i.to_s io
end
</code></pre></div></div>

<p>The ECR processor didn’t need to know or care that <code>10.times do |i|</code> started a new block. If there was a mis-matching <code>end</code>, that would be picked up by the actual Crystal compiler when the generated code is compiled. Syntax errors appear as coming from the ECR file because there are annotations that map the expressions in the generated code to the corresponding line and column number in the ECR file.</p>

<p>Anyway, we can totally do this compile-time-only stuff at runtime. Well, not actually. But mostly.</p>

<p>The ECR file is basically just a series of string literals separated by code snippets. We can’t change the code snippets at runtime, but the strings are fair game. I did wonder if you could do something where you wrap each code section in a <code>Proc</code> or conditional and if they get removed or re-ordered you could only evaluate the ones that remained in the file, but since they can have any inter-dependence (defining variables, etc) this would get very fragile very quickly. Although since like 95% of the time what I want to change is a misspelled HTML class attribute, being able to update the text content of the template is a huge improvement.</p>

<p>I wrote then re-wrote it a few times, and the end result is much simpler than I was expecting at the start. The most important thing is failing fast if the ECR file has changed in a way that we can’t render it anymore. Any change to the actual code will invalidate the template and the code will have to be recompiled to pick up the changes.</p>

<p><a href="https://codeberg.org/willhbr/geode/src/commit/279e1a79730243d5c8880675db828c4707e67c4f/src/geode/html_safe_ecr/runtime_html_ecr_processor.cr">The processor</a> that runs at compile time generates very similar code to the actual ECR processor. To check whether the code has changed, it builds a list of all the code snippets as strings. At the start of the generated code I call a helper method that takes this list, rereads the ECR file, and iterates through the tokens. If any code token is different or missing, the file has changed too much and we throw an exception. Otherwise we return a list of new strings that will replace the string literals. The generated code takes this list and inserts strings based on their index in the file (since that won’t change, since we’ll have failed already in that case).</p>

<p>Here’s the (slightly abridged) generated code for the ECR example above:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>strings = Geode::HTMLSafeECR::RuntimeLoader.get_strings(
  "test.ecr",
  [nil, " 1 &lt;&lt; 10 ", nil]
)
io &lt;&lt; strings[0]
(1 &lt;&lt; 10).to_html io
io &lt;&lt; strings[1]
</code></pre></div></div>

<p>The array passed to <code>get_strings</code> is generated from the original ECR file contents. Each <code>nil</code> is where there’s a string literal—something that can be replaced—and every non-<code>nil</code> string is a bit of code that must remain in the altered ECR file.<sup id="fnref:could-fail"><a href="#fn:could-fail" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>On release builds, all this code is removed and I swap back over to the boring compile-time-only processor, so all of this nonsense disappears and it works just like a normal ECR.</p>

<p>I’ve added this to the HTML-safe ECR generator in <a href="https://codeberg.org/willhbr/geode">Geode</a>—that I <a href="/2025/11/13/html-safe-ecr-templates/">wrote about the other day</a>—and have also simplified that code a whole bunch by splitting out the HTML-generating code into <code>to_html</code>, which removed the need for the <code>Builder</code> wrapper and <code>unsafe_write</code> method entirely. This has simplified the model of composable components, meaning that any object can override <code>to_html</code> and be inserted into an ECR template with <code>&lt;%= %&gt;</code>, and the escaping (or lack thereof) will work as you’d expect. You can see <a href="https://codeberg.org/willhbr/endash/commit/c90f8ef235b6104dc8b93a24a08e8b880acfe729">this commit in endash</a> as an example of swapping templates over to use this method.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:could-fail">
      <p>I should actually check the type of code block (whether it’s output or control or whatnot) but I haven’t been bothered yet. <a href="#fnref:could-fail" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/11/17/hot-ecr-reloading-in-your-area/">Permalink</a>
&bull; November 17, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Hot%20ECR%20Reloading%20in%20Your%20Area">email</a>.
]]></description>
      </item>
    
      <item>
        <title>HTML-Safe ECR Templates</title>
        <pubDate>Thu, 13 Nov 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/11/13/html-safe-ecr-templates/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/11/13/html-safe-ecr-templates/</guid>
        <description><![CDATA[<p>After writing <a href="/2025/11/07/tracking-down-progressively-enhanceable-apis/">my last post</a> I wondered if I could improve <code>ECR</code> to avoid the need to call <code>HTML.escape</code> explicitly when adding values to the template. This turned out to be much easier than I expected.</p>

<p>ECR (Embedded Crystal) is the compile-time templating system <a href="https://crystal-lang.org/api/1.18.2/ECR.html">included in the Crystal standard library</a>. The syntax is based on ERB (Embedded Ruby), but the template is processed at compile time in a macro, instead of at runtime. The end result is that using an ECR template is just the same performance-wise (good) as writing your output using <code>IO#&lt;&lt;</code> and <code>Object#to_s(IO)</code>. If we take this simple example:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;section&gt;
  &lt;h1&gt;&lt;%= title %&gt;&lt;/h1&gt;
  &lt;p&gt;&lt;%= content %&gt;&lt;/p&gt;
&lt;/section&gt;
</code></pre></div></div>

<p>It will get turned into roughly this code:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>io &lt;&lt; "&lt;section&gt;\n  &lt;h1&gt;"
title.to_s io
io &lt;&lt; "&lt;/h1&gt;\n  &lt;p&gt;"
content.to_s io
io &lt;&lt; "&lt;/p&gt;\n&lt;/section&gt;"
</code></pre></div></div>

<p>The problem is that <code>title</code> or <code>content</code> could contain HTML, like a naughty <code>&lt;script&gt;</code> tag, which would get dumped directly into our HTML document. The obvious thing to do is wrap every variable in <code>HTML.escape</code>:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;section&gt;
  &lt;h1&gt;&lt;%= HTML.escape title %&gt;&lt;/h1&gt;
  &lt;p&gt;&lt;%= HTML.escape content %&gt;&lt;/p&gt;
&lt;/section&gt;
</code></pre></div></div>

<p>Although as <a href="/2025/11/07/tracking-down-progressively-enhanceable-apis/">we learnt before</a> that will allocate a temporary string, just for the string to be written to the <code>IO</code> buffer and discarded—which is a waste—and more importantly the code is now ugly.</p>

<p>One option would be to make a custom <code>IO</code> subclass that processes every <code>IO#write</code> call through <code>HTML.escape</code>, ensuring that nothing is ever written with dangerous angle brackets. But that would mean that no HTML could be written, not even the HTML in our template file! Turning our <code>&lt;section&gt;</code> into a <code>&amp;lt;section&amp;gt;</code> is not very useful. We want string literals from the template to be left as-is, but everything else to be escaped.</p>

<p>You might look at the generated code and notice that values from the template are always passed to <code>IO#&lt;&lt;</code>, whereas variables are stringified using <code>to_s</code>, so maybe we could just override <code>&lt;&lt;</code> in our custom <code>IO</code>? That works right up until the point where a <code>to_s</code> call uses the <code>&lt;&lt;</code> method.</p>

<p>The remaining option is to see if I can hack with ECR itself.</p>

<p>Thankfully the ECR code is really modular, there are some macros in <a href="https://github.com/crystal-lang/crystal/blob/1c72aa8f20d40bfdfed1324df35bb33419b774e1/src/ecr/macros.cr"><code>macros.cr</code></a> that call <code>run</code> on <a href="https://github.com/crystal-lang/crystal/blob/1c72aa8f20d40bfdfed1324df35bb33419b774e1/src/ecr/process.cr"><code>ecr/process</code></a> which is just a wrapper around <a href="https://github.com/crystal-lang/crystal/blob/1c72aa8f20d40bfdfed1324df35bb33419b774e1/src/ecr/processor.cr"><code>ECR.process_string</code></a>, which just uses the <a href="https://github.com/crystal-lang/crystal/blob/1c72aa8f20d40bfdfed1324df35bb33419b774e1/src/ecr/lexer.cr"><code>ECR::Lexer</code></a> class to handle each token and build a string with the code.</p>

<p>I can just write my own macros to call my own processor (which can by 99% copied from the standard library) that uses the same <code>ECR::Lexer</code>. The difference is minimal: instead of calling <code>IO#&lt;&lt;</code>, I’ll call <code>unsafe_write</code>. This is a new method that I’ll add to a new <code>Builder</code> class that will handle the escaping. <code>Builder</code> will wrap an existing <code>IO</code> and any normal writes—that haven’t come from a template literal—will be fed through <code>HTML.escape</code>.</p>

<p>It’s actually really simple when you see it written down:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Builder &lt; IO
  def initialize(@io : IO)
  end

  def write(slice : Bytes) : Nil
    HTML.escape(slice, @io)
  end

  def unsafe_write(slice : Bytes)
    @io.write(slice)
  end
end
</code></pre></div></div>

<p>The generated code will only look slightly different:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>io.unsafe_write "&lt;section&gt;\n  &lt;h1&gt;"
title.to_s io
io.unsafe_write "&lt;/h1&gt;\n  &lt;p&gt;"
content.to_s io
io.unsafe_write "&lt;/section&gt;"
</code></pre></div></div>

<p>Now no matter what <code>title</code> and <code>content</code> are, they’re guaranteed to be passed through <code>HTML.escape</code> before being written to the wrapped <code>IO</code>.<sup id="fnref:more-benefits"><a href="#fn:more-benefits" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>However, what if we want to include a string as a piece of HTML? That’s also easy to do, I made a struct that wraps a <code>String</code> and checks the type of <code>IO</code> it’s writing to, changing the behaviour for the <code>Builder</code>:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>record SafeString, string : String do
  def to_s(io : IO)
    case io
    when Builder
      io.unsafe_write(self.string.to_slice)
    else
      self.string.to_s(io)
    end
  end
end
</code></pre></div></div>

<p>This is where Crystal’s type system lets me down a little. I’d like to either make this <code>HTMLSafe(T)</code> and wrap any type, or erase the type and use <code>Object</code> instead of <code>String</code>, but that’s not yet supported in Crystal.<sup id="fnref:object-var"><a href="#fn:object-var" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> Realistically this approach will work for the majority of cases, so it’s fine for now.</p>

<p>The next thing I would like to solve is a nice API for ECR layouts that have a gap for the main content. What I’ve done previously is render the main content into one buffer, turn that into a string, then render the layout and have the layout pull in the content string in the middle. The ideal would be system that’s as flexible as <a href="https://guides.rubyonrails.org/layouts_and_rendering.html"><code>content_for</code> in Rails</a> but rendering the whole page top-to-bottom with no intermediate buffers.</p>

<p>In the meantime I’ve added my HTML-escaping <code>ECR</code> to <a href="https://codeberg.org/willhbr/geode">Geode</a>, my little library of Crystal junk.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:more-benefits">
      <p>Splitting the literals from variables does open the possibility of counting the size of each string literal that is to be written to the <code>IO</code> and preemptively increasing the buffer size to fit them all. This would obviously get complicated with loops and conditionals, and Crystal’s macros aren’t too capable of extensive syntax-tree analysis, but it’s a possibility. <a href="#fnref:more-benefits" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:object-var">
      <p>“can’t use Object as the type of an instance variable yet, use a more specific type” <a href="#fnref:object-var" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/11/13/html-safe-ecr-templates/">Permalink</a>
&bull; November 13, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20HTML-Safe%20ECR%20Templates">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Tracking Down Progressively-Enhanceable APIs</title>
        <pubDate>Fri, 07 Nov 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/11/07/tracking-down-progressively-enhanceable-apis/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/11/07/tracking-down-progressively-enhanceable-apis/</guid>
        <description><![CDATA[<p>Something that I’m a big fan of is <a href="/2024/02/28/optimising-for-modification/">APIs that can be easily modified and hacked with</a>. It’s frustrating to have to write fully production-ready code when you actually want to just prototype something, and so I’m happy when there’s an API that it’s easy to use, which then progresses into a more full-featured API.</p>

<p>In Crystal an example of this is <code>File.read</code>. If you want to get the entire contents of a file you can just do:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>contents = File.read(path)
# process contents as a String
</code></pre></div></div>

<p>Will this cause problems if the file is huge? Probably, but it works fine for a prototype or in something less critical. Then when you want to be a grown-up and do things properly, the API doesn’t actually change that much:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>File.open(path) do |io|
  # use the IO to process the contents of the file
end
</code></pre></div></div>

<p>What I didn’t know is that there are a more of these APIs hiding in the Crystal stdlib that I wasn’t aware of. I found these after I captured a profile of my <a href="https://codeberg.org/willhbr/status_page">status page library</a>, it’s plenty fast enough (especially because I’m the only person ever sending requests) but I was interested to see what it spent its time doing.</p>

<p>I captured the sample using <a href="https://github.com/mstange/samply">samply</a> which is super convenient. I built it from the main branch so I could use the <code>--presymbolicate</code> flag. This dumps the symbol info directly into the profile file, since running a local web server and having the Firefox profiler talk back to it runs into all sorts of security roadblocks, especially when it’s not actually running locally.</p>

<p>To get some nice juicy data, I wrote another little program that would just spam a certain URL with HTTP requests, compiled with <code>--release -Dpreview_mt</code> to make the most of my cores:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>require "http/client"

uri = URI.parse ARGV[0]
path = uri.path

8.times do |i|
 Thread.new do
   client = HTTP::Client.new(uri)
   20000.times do
     client.get path
   end
   puts "Done #{i}"
 end
end

sleep
</code></pre></div></div>

<p>Unsurprisingly, after getting the profile I can confirm it spends its time dumping bytes into the response buffer, since there’s no interesting calculation on my default <code>/status</code> page. However there were two interesting sections that were taking more time than I expected.</p>

<p>The first is <code>HTML.escape</code>, which replaces characters that could be interpreted as HTML tags with the corresponding HTML entity. I noticed two things, first it immediately calls into <code>String#gsub</code>, and inside that there’s a <code>malloc</code> call. My first thought was maybe it’s implemented with a regex that’s convenient but not performant, but looking <a href="https://crystal-lang.org/api/1.18.2/HTML.html#escape%28string%3AString%29%3AString-class-method">at the docs</a> and the code, that’s not the case, it uses a <code>Hash(Char, String)</code> to make replacements with by looking up each character in the string with its replacement in the hash.</p>

<p>The <code>malloc</code> revealed the real issue though, <code>HTML.escape</code> takes a string and returns a new string with the escapes applied, and that new string has to be allocated. In my code I took that string and immediately dumped it into an <code>IO</code>:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@io &lt;&lt; ' ' &lt;&lt; key &lt;&lt; "=\"" &lt;&lt; HTML.escape(value.to_s) &lt;&lt; '"'
</code></pre></div></div>

<p>Look at that, I’m using the wrong API! That’s convenient, but it would be better if <code>HTML.escape</code> would write directly into <code>@io</code> instead of allocating its own buffer. Well that’s what <code>HTML.escape(string : String, io : IO)</code> does, and it’s trivial to swap:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@io &lt;&lt; ' ' &lt;&lt; k &lt;&lt; "=\""
HTML.escape(value.to_s, @io)
@io &lt;&lt; '"'
</code></pre></div></div>

<p>Re-profiled, and that section is gone from the trace. Easy.</p>

<p>Removing the non-<code>IO</code> API isn’t a good move here, since you’ll just encourage people to do this:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>escaped = String.build { |io| HTML.escape(value, io) }
</code></pre></div></div>

<p>Since what they <em>want</em> right now is a <code>String</code>, not a lecture about why not to allocate short-lived buffers.</p>

<p>The second thing that stood out were calls to <code>IO::Memory#increase_capacity_by</code>. <code>IO::Memory</code> is a dynamically-sized in-memory buffer, and the default capacity (as of Crystal 1.18.2) is just 64 bytes. When a write would exceed the size of the buffer, the capacity is increased to the next power of two with <code>Math.pw2ceil</code>, so as soon as we write our 65th byte, it’ll be increased to 128 bytes.</p>

<p>The response of a somewhat small status page is just under 5000 bytes, <sup id="fnref:wrong-assumptions"><a href="#fn:wrong-assumptions" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> so—assuming a bunch of small writes to the buffer—it will have to be expanded seven times. Since we know the response is going to be at least a few kilobytes (the template with no content is 1kB), setting the initial size to 4kB avoids having to do 6 reallocations.</p>

<p>In the end this is a non-issue, because the code is so low-traffic and already very performant. As <a href="/2023/07/09/limited-languages-foster-obtuse-apis/">I’ve written before</a> I don’t like the somewhat superstitious approach of limiting language features because someone might mis-use them. Slow code can come from anywhere, and you have to actually look for it. You can see the changes I actually made to the status page library <a href="https://codeberg.org/willhbr/status_page/commit/d312441c2ae93d270fd6fd615db5828273a4145b">in this commit</a>.</p>

<p>On the other hand, there is an opportunity for languages to enable library authors to guide their API use. Most languages have (either builtin or through a linter) a way of annotating that the return value from a function shouldn’t be ignored. In Rust this is the <code>#[must_use]</code> tag.</p>

<p>What could be neat is a system for attaching metadata to objects at compile time that could be read later in the compile step. So the <code>HTML.escape</code> method could attach a bit of information that says the string it returns could be directly written to an <code>IO</code>. Then if that object is passed to <code>IO#&lt;&lt;</code> (or <code>IO#write</code> or whatever) it could check that attribute and provide a warning.</p>

<p>This would fit in with Crystal’s existing macro system, but I’m sure it would explode the complexity of the compiler and make compilation much slower.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:wrong-assumptions">
      <p>I actually assumed it was under 4000 bytes without checking, and edited based on that. I should’ve just checked right away. <a href="#fnref:wrong-assumptions" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/11/07/tracking-down-progressively-enhanceable-apis/">Permalink</a>
&bull; November 7, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Tracking%20Down%20Progressively-Enhanceable%20APIs">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Recklessly Smashing mruby Into Zsh</title>
        <pubDate>Sat, 01 Nov 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/11/01/recklessly-smashing-mruby-into-zsh/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/11/01/recklessly-smashing-mruby-into-zsh/</guid>
        <description><![CDATA[<p>If you’re a reasonably serious shell user you probably know that you’ve got to write some things as shell functions instead of scripts because they need to modify the state of the shell itself. Usually that’s altering environment variables or changing the working directory of the shell. Often it’s just that you want to save some state for later and don’t want to deal with saving it to a file and parsing it back later.</p>

<p>I’ve got an old <a href="https://codeberg.org/willhbr/dotfiles/src/branch/main/shell/autoload/gcd">shell function called <code>gcd</code></a> that changes directory to a predefined location where I keep my projects. It also has autocomplete based on the project names. I originally write it with support for cloning repos, so you’d just do <code>gcd https://codeberg.org/willhbr/dotfiles.git</code> and it would grab the repo for you and put it in the right place. This ended up being more trouble than it was worth, because parsing a URL in a shell script is a pain, so I simplified it to just <code>cd</code> quickly with autocomplete.</p>

<p>Of course, implementing this in any scripting language (Ruby, Python, etc) would be easy. Even without proper URL parsing you can pretty quickly do a <code>.split '/'</code> and trim the <code>.git</code> off the end of the last element. But then that script couldn’t actually change the directory of your shell. You could just have a shell function that calls the script, captures the output, and calls <code>cd</code> itself, but then you’ve got two moving pieces that you’ve got to maintain, and you’ve got to make sure the script <em>only</em> prints the path as any extra output will mess it up.</p>

<p>So I had this terrible thought: Zsh and mruby are both just C programs, and mruby is <em>designed</em> to be compiled into other programs easily. Could I just run these in the same binary, and let a Ruby script do all the things a shell function can do?</p>

<p>The answer is no, not without a lot of effort, but I did see that it’s possible.</p>

<p>I started by grabbing the <a href="https://github.com/zsh-users/zsh">Zsh source code</a> (the GitHub mirror is much faster than the official server), running the <code>configure</code> script to generate a <code>Makefile</code>, and then spent ages messing around trying to reconcile the <a href="https://mruby.org/docs/articles/executing-ruby-code-with-mruby.html">mruby docs</a> with a huge <code>Makefile</code> that I didn’t understand. In the end I hacked it together like this:</p>

<ol>
  <li><a href="https://github.com/mruby/mruby">Clone <code>mruby</code></a> into the root of the Zsh repo (adding it to <code>.gitignore</code>)</li>
  <li>In the <code>mruby/</code> directory, run <code>make</code></li>
  <li>Add to <code>CPPFLAGS</code> and <code>LIBS</code> to point to <code>mruby</code></li>
</ol>

<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MRUBY_DIR = /path/to/my/projects/zsh/mruby
CPPFLAGS  = -I$(MRUBY_DIR)/include
LIBS      = -ldl -lncursesw -lrt -lm -lc -L$(MRUBY_DIR)/build/host/lib -lmruby
</code></pre></div></div>

<p>Then I could run <code>make</code> in the Zsh directory and get my very own binary in <code>Src/zsh</code>. Of course this is a complete hack because the Makefile is generated, so if you <em>actually</em> wanted to do this you’d have to work out how that fits together.</p>

<p>With the hard part out of the way, we can actually write some code to call Ruby. I found where builtin functions are defined (it’s <a href="https://github.com/zsh-users/zsh/blob/master/Src/builtin.c"><code>Src/builtin.c</code></a>) and copied an existing one to define a <code>require</code> function that would load a Ruby file.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#include "mruby.h"
#include "mruby/compile.h"

/* Builtins in the main executable */

static struct builtin builtins[] =
{
  BIN_PREFIX("-", BINF_DASH),
  BIN_PREFIX("builtin", BINF_BUILTIN),
  BIN_PREFIX("command", BINF_COMMAND),
  BIN_PREFIX("exec", BINF_EXEC),
  // ...
  BUILTIN("require", BINF_PSPECIAL, bin_mruby_require, 1, -1, 0, NULL, NULL),
</code></pre></div></div>

<p>I then found some existing code that would let me read a file—the <code>zstuff</code> documentation says “stuff a whole file into memory and return it” which is <em>exactly</em> what I needed. Most of this was hacked together by looking at other builtin Zsh functions.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/**/
int
bin_mruby_require(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))
{
  off_t len;
  char *s, *enam, *buf;
  struct stat st;
  mrb_value obj;
  if (!*argv)
      return 0;
  /* get arguments for the script */
  if (argv[1])
    pparams = zarrdup(argv + 1);

  enam = ztrdup(*argv);
  s = unmeta(enam);
  errno = ENOENT;
  if (access(s, F_OK) == 0 &amp;&amp; stat(s, &amp;st) &gt;= 0 &amp;&amp; !S_ISDIR(st.st_mode)) {
    len = zstuff(&amp;buf, s);
    obj = mrb_load_nstring(imruby, buf, len);

    if (imruby-&gt;exc) {
      obj = mrb_funcall(imruby, mrb_obj_value(imruby-&gt;exc), "inspect", 0);
      obj = mrb_funcall(imruby, obj, "to_s", 0);
      fwrite(RSTRING_PTR(obj), RSTRING_LEN(obj), 1, stdout);
      mrb_print_backtrace(imruby);
      putc('\n', stdout);
    }
  } else {
    return 1;
  }

  return 0;
}
</code></pre></div></div>

<p>What tripped me up for a while is that Zsh doesn’t have header files (at least not for the builtins) and I kept getting errors saying my function wasn’t defined. I’m used to writing civilised languages so I found this quite confusing. Eventually I noticed that every method had an empty doc comment (<code>/**/</code>) above it, and if I added that above my new method, it would get added to <code>builtin.epro</code>, which I assume is what they’re using as their headers.</p>

<p>We’re still not there yet, since we actually need to initialise a Ruby interpreter. Once again looking at how the rest of Zsh does things, I saw that functions and stuff were stored in a global <code>HashTable shfunctab</code>, defined in <code>hashtable.c</code>. I followed the pattern, defining my Ruby interpreter:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/**/
mod_export mrb_state* imruby;

/**/
void
init_mruby(void)
{
  imruby = mrb_open();
}
</code></pre></div></div>

<p>I then called this in the <code>setupvals</code> function in <code>Src/init.c</code>, so it would be available when I needed it. A quick <code>make</code> and I had a Zsh binary that could load Ruby code. That Ruby code couldn’t do much, but it would run.</p>

<p>Now here comes the actual hard part: calling Zsh from Ruby. This is where things get hairy, but this is the gist:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static mrb_value
mrb_call_zsh(mrb_state *mrb, mrb_value self)
{
  char *func_name;
  mrb_int argc = mrb_get_argc(mrb);
  const mrb_value *argv = mrb_get_argv(mrb);

  mrb_value func = mrb_cfunc_env_get(mrb, 0);
  func = mrb_funcall(imruby, func, "to_s", 0);
  func_name = RSTRING_PTR(func);

  LinkList args = znewlinklist();
  for (int i = 0; i &lt; argc; i++) {
    mrb_value arg = argv[i];
    arg = mrb_funcall(imruby, arg, "to_s", 0);
    char *str = RSTRING_PTR(arg);
    zaddlinknode(args, str);
  }
  Builtin bf;
  Shfunc shf;
  if ((shf = (Shfunc)
    shfunctab-&gt;getnode(shfunctab, func_name))) {
    lastval = doshfunc(shf, args, 1);
  } else if ((bf = (Builtin)
          builtintab-&gt;getnode(builtintab, func_name))) {
    LinkList a;
    execbuiltin(args, a, bf);
  } else {
    lastval = 127;
    zputs(func_name, stdout);
    zputs(" not found\n", stdout);
  }

  return self;
}

static mrb_value
mrb_zsh_lookup(mrb_state *mrb, mrb_value self)
{
  mrb_value env[1];
  env[0] = mrb_get_arg1(mrb);

  struct RProc *proc = mrb_proc_new_cfunc_with_env(mrb, mrb_call_zsh, 1, env);
  return mrb_obj_value(proc);
}

// In init_mruby()
mrb_define_method(imruby, imruby-&gt;kernel_module,
            "zsh_lookup", mrb_zsh_lookup, MRB_ARGS_REQ(1));
</code></pre></div></div>

<p>This allows a Ruby script to lookup and call a Zsh function by name. If you defined a function in Zsh like this:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my_zsh_function() {
  echo "Hello from Zsh!"
}
</code></pre></div></div>

<p>You can get the function and call it from Ruby:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>zsh_lookup('my_zsh_function').call(nil)
</code></pre></div></div>

<p>It doesn’t handle arguments properly, but that’s just the start of the problems.</p>

<p>The reason this wouldn’t work is that you’re merging two programming languages, each with their own object model, garbage collector, and suchlike, and trying to make them work as one. This creates a whole host of awkward questions.</p>

<p>Like how do you determine when an object should be freed? You’ll need to track what has a reference to it on both sides. Zsh functions don’t return objects, they output text. What happens when you call a Zsh function from Ruby, do you need to do something special to capture the output, or should the output be returned as a string by default? How do you change that behaviour? If you pass a complex Ruby object to a Zsh function, how does that work? Does it get converted into a string? Is it only when you try to read it in Zsh that it becomes a string, but if it gets passed from Ruby to Zsh then back to Ruby it’ll stay the same? What if it gets passed to a command as an argument?</p>

<p>None of these questions are unanswerable, but answering them and implementing a consistent behaviour that’s bug-free would be a significant amount of work.</p>

<p>There are some projects that are leaning in this direction already. <a href="https://fishshell.com">Fish shell</a> is the obvious “shell but with a more modern language”. <a href="https://xon.sh/"><code>xonsh</code></a> basically merges a shell with Python, and <a href="https://www.oilshell.org/"><code>oil</code></a> adds their own proper scripting language into a modern shell. <a href="/2023/07/06/why-modernising-shells-is-a-sisyphean-effort/">I wrote about these and the challenges of modernising shells in 2023</a>.</p>

<p>Ultimately though I want to keep using Zsh, with all its features and plugins and extensibility, and then also use my preferred scripting language to write helper functions and autocomplete. Think about all the times a “simple” shell script has to use <code>grep</code> and <code>sed</code> and <code>awk</code> just to do things that are trivial in any scripting language. I’ve even started <a href="/2025/06/08/using-ruby-in-shell-pipelines/">using Ruby as a replacement for these tools because I find it easier</a>.</p>

<p>If you’re going to write a script, unless it’s only a few lines just use a real scripting language. I don’t really care which one.<sup id="fnref:i-totally-do"><a href="#fn:i-totally-do" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> Most OSes have good scripting languages built in, so you probably don’t have to worry about portability that much. It’ll be easier to parse arguments, report errors, and do data processing. It’ll be easier to add a new feature or edge case. And when you’re finally ready to admit it needs to become a <em>program</em> instead of a <em>script</em>, it’s already structured like one and it’ll be easier to migrate to another language.<sup id="fnref:use-rust"><a href="#fn:use-rust" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:i-totally-do">
      <p>I totally care, it should be Ruby, because Ruby rules and is great for scripting. <a href="#fnref:i-totally-do" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:use-rust">
      <p>Honestly <a href="https://docs.rs/clap/latest/clap/">Clap</a> is so good at providing consistent argument parsing and error messages it makes Rust the obvious choice for writing command-line programs. Crystal is a top contender because it’s the <a href="/2023/06/24/why-crystal-is-the-best-language-ever/">best language ever</a>. <a href="#fnref:use-rust" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/11/01/recklessly-smashing-mruby-into-zsh/">Permalink</a>
&bull; November 1, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Recklessly%20Smashing%20mruby%20Into%20Zsh">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Unlocking the Full Speed of the tmux Interpreter</title>
        <pubDate>Sat, 25 Oct 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/10/25/full-speed-tmux-interpreter/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/10/25/full-speed-tmux-interpreter/</guid>
        <description><![CDATA[<p>This will take a little bit of catching up. In 2024 I worked out <a href="/2024/03/15/making-a-compiler-to-prove-tmux-is-turing-complete/">how to compile code to run in tmux</a> by swapping between windows and setting hooks to run when each window got focus. Immediately after that I realised the whole window thing was unnecessary, and you <a href="/2024/03/16/further-adventures-in-tmux-code-evaluation/">could use key bindings and nested sessions</a> to simplify the whole thing. Then at the end of the year I use this method to <a href="/2024/12/27/solving-sudoku-with-tmux/">solve a sudoku entirely within a tmux config file</a>.<sup id="fnref:other-projects"><a href="#fn:other-projects" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>All of these relied on running a shell command just to run <code>tmux</code> again in order to have variable expansion in places where you wouldn’t normally get it. You can put tmux variables in the argument to <code>run-shell</code> and they’ll be expanded before the command is run. I then realised that I’d <a href="/2025/01/10/how-did-i-miss-run-c/">missed an option to the <code>run</code> command</a> where you can just directly run a <code>tmux</code> command without going to a shell at all, while still getting the same expansion.</p>

<p>Not having to start a new subprocess for every “clock cycle” speeds the execution up <em>dramatically</em>, so much so that tmux isn’t able to keep up with all the keypresses, and they will be occasionally dropped and sent through to the shell, instead of being processed by tmux. So while it gives a 4× speed increase, it also makes the “program” unreliable to the point that I couldn’t really run it without having to manually prod it to get it to complete.</p>

<p>Well I’ve just learnt about yet another tmux feature that fixes this exact problem!</p>

<p>The <code>send-keys</code> command has a <code>-K</code> flag,<sup id="fnref:its-new"><a href="#fn:its-new" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> which means the input is interpreted as though it came from a client instead of being directed into a pane. Previously I needed to have tmux <a href="/2024/03/16/further-adventures-in-tmux-code-evaluation/">attached back to itself</a>—a session with two windows and the second window is attached to the same session looking at the first window—but this option does the same thing with none of that mess.</p>

<p>It does have a wrinkle in that the client IDs are not predictable, unlike sessions, windows, or panes. At least I don’t think so. They’re just identified by the pseudo-terminal that is being used (or something, I don’t really understand how this works) so they look like <code>/dev/pts/0</code>. I can’t predict what it’ll be because other programs or tmux sessions might be using some IDs.</p>

<p>Thankfully this is not a problem, as we can just expand the <code>#{client_tty}</code> variable to get the current client, and use <code>run -C</code> to expand that variable in the <code>send-keys</code> command. So to send <code>n</code> we would just:</p>

<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code>run -C "send-keys -K -c '#{client_tty}' n"
</code></pre></div></div>

<p>This doesn’t have to start a subprocess, and tmux doesn’t miss the key input. It’s no faster than the <a href="/2025/01/10/how-did-i-miss-run-c/">original <code>run -C</code> speedup</a>, but it’s completely reliable. I can solve a sudoku with 5 missing numbers in 38 seconds, down from 6:10 when using subprocesses, and just as reliable.</p>

<p>You can get the updated code <a href="https://codeberg.org/willhbr/tmux-sudoku">on Codeberg</a> if you want to benchmark it yourself.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:other-projects">
      <p>And then obviously <a href="/2025/03/17/playing-video-with-5170-tmux-windows/">playing video</a> and <a href="/2025/03/20/snakes-in-a-pane/">playing snake</a>. <a href="#fnref:other-projects" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:its-new">
      <p>It was added in tmux 3.4, so it was added after I wrote the compiler (I was using 3.3a), which is likely why I hadn’t seen this option in my original readings of the tmux manual. <a href="#fnref:its-new" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/10/25/full-speed-tmux-interpreter/">Permalink</a>
&bull; October 25, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Unlocking%20the%20Full%20Speed%20of%20the%20tmux%20Interpreter">email</a>.
]]></description>
      </item>
    
      <item>
        <title>Light Mode InFFFFFFlation</title>
        <pubDate>Mon, 20 Oct 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/10/20/light-mode-infffffflation/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/10/20/light-mode-infffffflation/</guid>
        <description><![CDATA[<p>Back in the day, light mode wasn’t called “light mode”. It was just the way that computers were, we didn’t really think about turning everything light or dark. Sure, some applications were often dark (photo editors, IDEs, terminals) but everything else was light, and that was fine.</p>

<p>What we didn’t notice is that light mode has been slowly getting lighter, and I’ve got a graph to prove it. I did what any normal person would do, I downloaded the same (or similar) screenshots from the <a href="https://512pixels.net/projects/aqua-screenshot-library/">MacOS Screenshot Library</a> on <a href="https://512pixels.net/"><em>512 Pixels</em></a>. This project would have been much more difficult without a single place to get well-organised screenshots from. I cropped each image so just a representative section of the window was present, here shown with a pinkish rectangle:</p>

<p><img loading="lazy" src="/images/2025/macos-light-mode.webp" alt="screenshot of OS X Snow Leopard Finder window with toolbar section highlighted" /></p>

<p>Then used <a href="https://pypi.org/project/pillow/">Pillow</a> to get the average lightness of each cropped image:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>for file in sorted(os.listdir('.')):
  image = Image.open(file)
  greyscale = image.convert('L')
  stat = ImageStat.Stat(greyscale)
  avg_lightness = int(stat.mean[0])
  print(f"{file}\t{avg_lightness}")
</code></pre></div></div>

<p>This ignores any kind of perceived brightness or the tinting that MacOS has been doing for a while based on your wallpaper colour. I could go down a massive tangent trying to work out exactly what the best way to measure this is, but given that the screenshots aren’t perfectly comparable between versions, comparing the average brightness of a greyscale image seems reasonable.</p>

<p>I graphed that on the release year of each OS version, doing the same for dark mode:</p>

<svg viewBox="0 -5 365 290" xmlns="http://www.w3.org/2000/svg">
  <style>
    polyline { fill: none; }
    polyline, line { stroke-width: 3px; stroke-linecap: round; }
    text { font-family: "system-ui"; fill: var(--tec); }
  </style>
  <defs>
    <linearGradient id="gradient" x1="0" x2="0" y1="0" y2="1">
      <stop offset="0%" stop-color="white" />
      <stop offset="100%" stop-color="black" />
    </linearGradient>
  </defs>
  <rect id="rect1" x="0" y="0" rx="3" ry="3" width="320" height="255" fill="url(#gradient)" />
  <polyline style="stroke: #6609a0" points="
    0,74
    40,58
    60,61
    80,53
    100,25
    120,24
    140,24
    160,24
    180,33
    200,33
    220,6
    240,6
    260,3
    280,10
    300,11
    320,0"></polyline>
  <polyline style="stroke: #bf8eff" points="
      180,202
      200,197
      220,200
      240,199
      260,210
      280,215
      300,200
      320,215"></polyline>
  <text x="0" y="270" textLength="40">2009</text>
  <text x="280" y="270" textLength="40">2025</text>
  <text x="325" y="10" textLength="40">100%</text>
  <text x="325" y="255">0%</text>
</svg>

<p class="caption">This graph is an SVG, which may not render correctly in feed readers. <a href="/2025/10/20/light-mode-infffffflation/">View this post on the web</a>.</p>

<p>You can clearly see that the brightness of the UI has been steadily increasing for the last 16 years. The upper line is the default mode/light mode, the lower line is dark mode. When I started using MacOS in 2012, I was running Snow Leopard, the windows had an average brightness of 71%. Since then they’ve steadily increased so that in MacOS Tahoe, they’re at a full 100%.</p>

<p>What I’ve graphed here is just the brightness of the window chrome, which isn’t really representative of the actual total screen brightness. A better study would be looking at the overall brightness of a typical set of apps. The default background colour for windows, as well as the colours for inactive windows, would probably give a more complete picture.</p>

<p>For example, <a href="https://512pixels.net/projects/aqua-screenshot-library/macos-26-tahoe/">in Tahoe</a> the darkest colour in a typical light-mode window is the colour of a section in an inactive settings window, at 97% brightness. In Snow Leopard the equivalent colour was 90%, and that was one of the <em>brightest</em> parts of the window, since the window chrome was typically darker than the window content.</p>

<p>I tried to remember exactly when I started using dark mode all the time on MacOS. I’ve always used a dark background for my editor and terminal, but I wasn’t sure when I swapped the system theme across. When it first came out I seem to remember thinking that it looked gross.</p>

<p>It obviously couldn’t be earlier than 2018, as that’s when dark mode was introduced in <a href="https://en.wikipedia.org/wiki/MacOS_Mojave">MacOS Mojave</a>. I’m pretty sure that when I updated my personal laptop to an M1 MacBook Air at the end of 2020 that I set it to use dark mode. This would make sense, because the <a href="https://en.wikipedia.org/wiki/MacOS_Big_Sur">Big Sur</a> update bumped the brightness from 85% to 97%, which probably pushed me over the edge.</p>

<p>I think the reason this happens is that if you look at two designs, photos, or whatever, it’s really easy to be drawn in to liking the brighter one more. Or if they’re predominantly dark, then the darker one. I’ve done it myself with this very site. If I’m tweaking the colours it’s easy to bump up the brightness on the background and go “ooh wow yeah that’s definitely cleaner”, then swap it back and go “ewww it looks like it needs a good scrub”. If it’s the dark mode colours, then a darker background will just look <em>cooler</em>.</p>

<p>I’m not a designer, but I assume that resisting this urge is something you learn in design school. Just like making a website look good with a non-greyscale background.</p>

<p>This year in iOS 26, some UI elements use the HDR screen to make some elements and highlights brighter than 100% white.<sup id="fnref:not-me"><a href="#fn:not-me" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> This year it’s reasonably subtle, but the inflation potential is there. If you’ve ever looked at an HDR photo on an iPhone (or any other HDR screen) then looked at the UI that’s still being shown in SDR, you’ll know just how grey and sad it looks. If you’re designing a new UI, how tempting will it be to make just a little bit more of it just a little bit brighter?</p>

<p>As someone whose job involves looking at MacOS for a lot of the day, I find that I basically <em>have</em> to use dark mode to avoid looking at a display where all the system UI is 100% white blasting in my eyes. But the alternative doesn’t have to be near-black for that, I would happily have a UI that’s a medium grey. In fact what I’ve missed since swapping to using dark mode is that I don’t have contrast between windows. Everything looks the same, whether it’s a text editor, IDE, terminal, web browser, or Finder window. All black, all the time.</p>

<p>Somewhat in the spirit of <a href="https://mavericksforever.com">Mavericks Forever</a><sup id="fnref:popup-on-scroll"><a href="#fn:popup-on-scroll" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>, if I were to pick an old MacOS design to go back to it would probably be <a href="https://512pixels.net/projects/aqua-screenshot-library/os-x-10-10-yosemite/">Yosemite</a>. I don’t have any nostalgia for skeuomorphic brushed metal or stitched leather, but I do quite like the flattened design and blur effects that Yosemite brought. Ironically Yosemite was a substantial jump in brightness from previous versions.</p>

<p>So if you’re making an interface or website, be bold and choose a 50% grey. My eyes will thank you.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:not-me">
      <p>I’m still happily on iOS 18 so I’ve not used this myself. <a href="#fnref:not-me" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:popup-on-scroll">
      <p>Whoever came up with the design trend where each block on a webpage pops up into view after you scroll past it needs to pay for their crimes. <a href="#fnref:popup-on-scroll" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/10/20/light-mode-infffffflation/">Permalink</a>
&bull; October 20, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20Light%20Mode%20InFFFFFFlation">email</a>.
]]></description>
      </item>
    
      <item>
        <title>The 6.2nd Stage of Swift on Linux</title>
        <pubDate>Mon, 13 Oct 2025 00:00:00 +1100</pubDate>
        <link>https://willhbr.net/2025/10/13/the-6-2nd-stage-of-swift-on-linux/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/10/13/the-6-2nd-stage-of-swift-on-linux/</guid>
        <description><![CDATA[<p>Back in 2023 I wrote <a href="/2023/08/23/the-five-stages-of-swift-on-linux/"><em>The Five Stages of Swift on Linux</em></a> where I went through all the stages of grief while trying to get a working websocket connection on Swift on Linux. What I ended up finding out was that the Swift websocket implementation on Linux relied on websockets in <code>libcurl</code>, which <a href="https://github.com/apple/swift-corelibs-foundation/issues/4730#issuecomment-1613801914">at the time were experimental</a> and so weren’t available.</p>

<p>Well here we are over two years later and on the Swift blog there’s a <a href="https://www.swift.org/blog/swift-on-the-server-ecosystem/">post titled <em>The Growth of the Swift Server Ecosystem</em></a>. So I guess I should see if we have websockets yet.</p>

<p>I grab a container, write a quick test server in Crystal,<sup id="fnref:crystal-websocket"><a href="#fn:crystal-websocket" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> and grab the code from my original post:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import Foundation
import FoundationNetworking

let task = URLSession.shared.webSocketTask(
  with: URL(string: "ws://host.containers.internal:9080")!)
task.resume()
try! await task.send(.string("test message"))
</code></pre></div></div>

<p>I’m running this on the latest Docker image (<code>swift:latest</code>) which is based on Ubuntu 24.04.3 (that’ll be important later) and here’s what I get:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code>swift_test/swift_test.swift:26: Fatal error: 'try!' expression unexpectedly raised an error:
  Error Domain=NSURLErrorDomain Code=-1002
  "(null)"UserInfo={
    NSLocalizedDescription=WebSockets not supported by libcurl,
    NSErrorFailingURLStringKey=ws://host.containers.internal:9080,
    NSErrorFailingURLKey=ws://host.containers.internal:9080}

💣 Program crashed: Illegal instruction at 0x0000791e0e6e26b8

Platform: x86_64 Linux (Ubuntu 24.04.3 LTS)

Thread 1 crashed:

  0 0x0000791e0e6e26b8 _assertionFailure(_:_:file:line:flags:) + 264 in libswiftCore.so
  1 0x0000791e0e716d76 swift_unexpectedError + 805 in libswiftCore.so
  2 async_MainTY2_ + 74 in swift-test at /src/Sources/swift-test/swift_test.swift:26:6

    24│ task.resume()
    25│ print("created task...")
    26│ try! await task.send(.string("test message"))
      │      ▲
    27│ print("sent message")
    28│
</code></pre></div></div>

<p>Compared to last time, this is a <em>huge</em> improvement. Not only have I got an error message instead of just an error code, it actually explains why the connection failed with a pointer to the cause—the <code>libcurl</code> version.</p>

<p>So the latest Swift release doesn’t support websockets out of the box on Linux just yet—but can we get it working?</p>

<p>The version of <code>libcurl</code> on Ubuntu 24.04.3 is 8.5, and we need <a href="https://curl.se/ch/8.11.0.html">at least 8.11 to get websocket support</a>. Updating the package through <code>apt</code> doesn’t work (the latest you’ll get is 8.5) so instead I created a new container with an Ubuntu 25.04 image, then ran <code>apt update &amp;&amp; apt install curl</code>, which got me 8.12.1. I then had to go through the full <a href="https://www.swift.org/install/linux/">Swift install process</a>, only then could I run my websocket, and it worked perfectly.</p>

<p>Or I assume it did, I’d actually forgotten to put any logging in the Crystal websocket server, so it wouldn’t actually print anything when a client connected. So I didn’t know whether the Swift client had actually worked. I updated the server and re-ran the client and it worked that time.</p>

<p>My understanding is that Swift dynamically links its runtime and runtime dependencies, so I don’t just need a specific <code>libcurl</code> version to build my program, that needs to be available wherever this code is running. This just seems like a real pain, I’m a big fan of just statically linking everything into one binary so this isn’t a concern at all.</p>

<p>Thankfully <a href="https://www.swift.org/documentation/articles/static-linux-getting-started.html">Swift 6 supports static linking</a>. The install process isn’t super clear, the example in that post is now outdated, so you need to go to the <a href="https://www.swift.org/install/linux/">install page</a> and find the “Static Linux” download in the “Swift SDK Bundles” section. That gives you a <code>swift sdk install</code> command that will do the download for you. For 6.2 that’s:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ swift sdk install https://download.swift.org/swift-6.2-release/static-sdk/swift-6.2-RELEASE/swift-6.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz \
  --checksum d2225840e592389ca517bbf71652f7003dbf45ac35d1e57d98b9250368769378
</code></pre></div></div>

<p>You can then compile a static binary with:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code>swift build --swift-sdk x86_64-swift-linux-musl
</code></pre></div></div>

<p>Which we can then take to any <code>amd64</code> Linux machine and run:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./swift-test
running...
created task...
swift_test/swift_test.swift:26: Fatal error: 'try!' expression unexpectedly raised an error:
  Error Domain=NSURLErrorDomain Code=-1002
  "(null)"UserInfo={
    NSErrorFailingURLStringKey=ws://host.containers.internal:9080,
    NSLocalizedDescription=WebSockets not supported by libcurl,
    NSErrorFailingURLKey=ws://host.containers.internal:9080}
Current stack trace:
0    &lt;unknown&gt;                          0x0000000000a5e56c
1    &lt;unknown&gt;                          0x0000000000b08631
2    &lt;unknown&gt;                          0x000000000070a4ba
3    &lt;unknown&gt;                          0x00000000008bfa73
4    &lt;unknown&gt;                          0x00000000008f4b62
5    &lt;unknown&gt;                          0x00000000006c9acb
6    &lt;unknown&gt;                          0x0000000000b51633
7    &lt;unknown&gt;                          0x0000000000b527ce
8    &lt;unknown&gt;                          0x0000000000b6afba
9    &lt;unknown&gt;                          0x0000000000b6b7d8
10   &lt;unknown&gt;                          0x0000000000b70666
Illegal instruction (core dumped)
</code></pre></div></div>

<p>Wait no that’s not what it’s supposed to do.</p>

<p>I guess we statically linked in the wrong <code>libcurl</code> version? How can we tell?</p>

<p>That <a href="https://www.swift.org/documentation/articles/static-linux-getting-started.html">static Swift post</a> has a command output at the bottom that shows a bunch of library versions—including <code>curl</code>. It’s for Swift 6.1 though, so it’s outdated. We’ll need to run the same thing ourselves on the 6.2 SDK.</p>

<p>They don’t link to the tool, and <code>bom</code> is generic enough that it’s hard to be sure what they’re talking about, but it’s the <a href="https://github.com/kubernetes-sigs/bom">Kubernetes <code>bom</code> tool</a>, which to install you first need to install <a href="https://go.dev/doc/install"><code>go</code></a>, set <code>$GOPATH</code>, and then finally you get a <code>bom</code> binary.<sup id="fnref:assume-static"><a href="#fn:assume-static" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ bom document outline ~/.swift-sdks/swift-6.2-RELEASE_static-linux-0.0.1.artifactbundle/sbom.spdx.json
               _
 ___ _ __   __| |_  __
/ __| '_ \ / _` \ \/ /
\__ \ |_) | (_| |&gt;  &lt;
|___/ .__/ \__,_/_/\_\
    |_|

 📂 SPDX Document SBOM-SPDX-f4e4b6d7-adb7-4694-a4b3-75b5c1eadeca
  │
  │ 📦 DESCRIBES 1 Packages
  │
  ├ Swift statically linked SDK for Linux@0.0.1
  │  │ 🔗 7 Relationships
  │  ├ GENERATED_FROM PACKAGE swift@6.2-RELEASE
  │  ├ GENERATED_FROM PACKAGE musl@1.2.5
  │  ├ GENERATED_FROM PACKAGE musl-fts@1.2.7
  │  ├ GENERATED_FROM PACKAGE libxml2@2.12.7
  │  ├ GENERATED_FROM PACKAGE curl@8.7.1
  │  ├ GENERATED_FROM PACKAGE boringssl@fips-20220613
  │  └ GENERATED_FROM PACKAGE zlib@1.3.1
  │
  └ 📄 DESCRIBES 0 Files
</code></pre></div></div>

<p>So the Swift 6.2 SDK ships with <code>curl@8.7.1</code>, so no websocket support yet.</p>

<p>I suppose that maybe you could build your own SDK to include a more recent version version of <code>libcurl</code>, but at that point you’re already an edge case (statically compiled Swift) of an edge case (Swift on Linux); so you might as well just <a href="/2025/07/25/rewriting-pod-with-wisdom/">rewrite it in Rust</a>.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:crystal-websocket">
      <p>Networking in Crystal is so easy for me to write and incredibly reliable. <a href="#fnref:crystal-websocket" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
    <li id="fn:assume-static">
      <p>Which I assume is statically linked. <a href="#fnref:assume-static" class="reversefootnote" role="doc-backlink">&uarr;</a></p>
    </li>
  </ol>
</div>


<a href="https://willhbr.net/2025/10/13/the-6-2nd-stage-of-swift-on-linux/">Permalink</a>
&bull; October 13, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20The%206.2nd%20Stage%20of%20Swift%20on%20Linux">email</a>.
]]></description>
      </item>
    
      <item>
        <title>It Wasn&apos;t a Virus, It Was Just TLS</title>
        <pubDate>Sun, 05 Oct 2025 00:00:00 +1000</pubDate>
        <link>https://willhbr.net/2025/10/05/it-wasnt-a-virus-it-was-just-tls/</link>
        <guid isPermaLink="true">https://willhbr.net/2025/10/05/it-wasnt-a-virus-it-was-just-tls/</guid>
        <description><![CDATA[<p>This has been going on for like a year. Every time I run an HTTP server to preview something, I get a bunch of junk data sent to it:</p>

<pre><code>100.73.68.18 - - [16/Aug/2025 19:55:15] code 400, message Bad HTTP/0.9 request type
('\\x16\\x03\\x01\\x02\\x00\\x01\\x00\\x01ü\\x03\\x03W#Ø7e¬(ÛãÙþ&gt;-cÞ±öý^rO7\\x83=æÁ\\x8bÑ"ÎD\\x97')
</code></pre>

<p>I had no idea where this was coming from. Some background process on my laptop? A device on my network? A browser extension gone rogue?</p>

<p>Clearly I wasn’t actually <em>that</em> concerned about it since I have ignored it for so long. But then this week I noticed that the same thing was happening with my Caddy server:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"Unsolicited response received on idle HTTP channel starting with \"\\x1f\\x8b\\b\\x00i\\b\\x9fh\\x00\\xff+N\\xccM\\xe5\\x02\\x00\\xebÓšC\\x05\\x00\\x00\\x00\"; err=&lt;nil&gt;"
</code></pre></div></div>

<p>I <a href="/2025/03/09/a-slim-home-server-with-alpine-linux/">run Caddy as an HTTP proxy</a> in order to get sensible domain names for my self-hosted services, so this traffic had to be coming from one of my own devices.</p>

<p>It finally boiled over and I had to get to the bottom of it. So I started in the only way I know how: writing a server in Crystal.</p>

<p>The Python (and Caddy) servers were telling me they were receiving invalid HTTP requests, but they didn’t give me much more to go on. Was it invalid data passed during an HTTP request, was it sent before or after the successful request?</p>

<p>A real HTTP server would obfuscate most of this detail, so instead I used the Crystal <code>TCPServer</code> class to make a simple debugging view that showed more info about the actual HTTP data being written. All lines received would be printed, and every request would get the same HTTP 302 redirect response.</p>

<p>Servers in Crystal can be shockingly simple:</p>

<div class="language-crystal highlighter-rouge"><div class="highlight"><pre class="highlight"><code>require "socket"

OK = "
HTTP/1.1 302
Location: http://endash/
".strip

server = TCPServer.new("0", 80)

id = 0
while client = server.accept?
  id += 1
  new_id = id
  spawn do
    puts "Connection #{new_id}: #{client.inspect}"
    handle_client(new_id, client)
    while line = client.gets
      puts "#{new_id.to_s.rjust(3)}: #{line.inspect}"
      break if line.empty?
    end
    client.puts OK
    client.close
  end
end
</code></pre></div></div>

<p>The server would read lines from the client, printing each line, and when it got to an empty line (the end of the headers) it would write the canned response and close the connection. Each request would be prefixed with the client number so I could differentiate between the connections that were established.</p>

<p>Using this and some prior debugging with the Python server I could pin down the behaviour:</p>

<ul>
  <li>The first request from Safari always triggered the garbage</li>
  <li>The second request to the same (host, port) did not</li>
  <li>No requests from Firefox triggered it</li>
  <li>Using Safari after initially sending a request from Firefox would still trigger it</li>
</ul>

<p>This meant it was something specific to Safari, not related to the OS or another device (I should have noted initially that the IP the request was coming from was always my laptop, which would have ruled out any other device). Since it didn’t happen in Firefox, and using Firefox didn’t change Safari’s behaviour then it was unlikely to be any shared infrastructure in the OS.</p>

<p>Next I turned off my any browser extensions and used a private browsing window, and still got the same behaviour. It’s reassuring that some extension wasn’t sending nonsense traffic around.</p>

<p>By assigning an ID to each TCP connection, I could see the order in which each connection was established, and then look at the order that the data was received to get a bit better picture of what the request flow was. Interestingly the socket for the legitimate HTTP request was always opened first—and so got the first ID—but the garbage data was what ended up being received first. I didn’t really know what to make of this, but it’s an interesting data point that’ll be useful later.</p>

<p>Doing a web search for the first section of bytes didn’t yield anything useful—basically no meaningful results. What did get me a result was using an LLM—I do want to pattern match on a series of tokens after all. I asked “I’m getting a weird non-http request to a development server from Safari on MacOS, it’s sending this data, what is it?” and included the first chunk of the request bytes (escaped). Chatty G responded:</p>

<blockquote>
  <p>That blob of data you pasted isn’t an HTTP request at all — it’s the start of a TLS ClientHello message.</p>
</blockquote>

<p>Aha! There we go! A further search led me to <a href="https://tls12.xargs.org/#client-hello">this excellent explanation</a> that details every part of the <code>ClientHello</code> message. This makes sense, and I am always seeing that <code>16 03 01</code> at the start of the message, which is the important part that says “this is a TLS <code>ClientHello</code> message”.</p>

<p>That’s not a virus, Safari is just trying to use TLS on a host that doesn’t support TLS. But why does Safari do this when the server is plain HTTP?</p>

<p>One possibility is <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security">HTTP Strict Transport Security</a> (HSTS), where the server can set a header and the browser will then refuse to use plain HTTP for a certain time period. I thought that because the server was on a Tailscale “MagicDNS” domain, maybe they’d set a wildcard HSTS policy, but if that were the case then the browser should refuse to connect at all, rather than just attempting a TLS connection and giving up.</p>

<p>What seems to be the answer is that Safari tries fairly aggressively to upgrade the connection to HTTPS, whereas Firefox does not. If the server had responded back to that <code>ClientHello</code> message, I would have been directed to an HTTPS site without my server having to do a redirect itself.</p>

<p>So there you go. I wasn’t hacked, Safari was just trying to be helpful.</p>


<a href="https://willhbr.net/2025/10/05/it-wasnt-a-virus-it-was-just-tls/">Permalink</a>
&bull; October 5, 2025
&bull; Send feedback via
  <a href="https://ruby.social/@willhbr" target=_blank>mastodon</a> or
  
  <a href="mailto:feedback@willhbr.net?subject=Feedback:%20It%20Wasn't%20a%20Virus,%20It%20Was%20Just%20TLS">email</a>.
]]></description>
      </item>
    
  </channel>
</rss>
