<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title>null program</title>
  <link rel="alternate" type="text/html" href="https://nullprogram.com"/>
  <link rel="self" type="application/atom+xml" href="https://nullprogram.com/feed/"/>
  <updated>2026-11-05T00:29:59Z</updated>
  <id>urn:uuid:f8b65823-4ec5-3a70-efc8-2b713aa63091</id>

  <author>
    <name>Christopher Wellons</name>
    <uri>https://nullprogram.com/</uri>
    <email>wellons@nullprogram.com</email>
  </author>

  <entry>
    <title>We can do better than line-buffered output</title>
    <link rel="alternate" type="text/html" href="https://nullprogram.com/blog/2026/11/05/"/>
    <id>urn:uuid:b62a4635-27cc-8d16-8e4a-6e39942cdcc4</id>
    <updated>2026-11-05T00:25:25Z</updated>
    <category term="c"/>
    <content type="html">
      <![CDATA[<p><a href="/blog/2023/02/13/">Buffered output</a> typically comes in two flavors: fully buffered
and line buffered. If output goes to a terminal then the implementation
automatically selects line buffering, flushing its buffer when a line is
completed so that it reaches the user immediately. Otherwise it fully
buffers, holding output until the buffer is full or an explicit flush
event at some arbitrary point in the future. Line buffering is a common
bottleneck in non-interactive terminal applications such that it’s worth
redirecting output to a file or pipe in order to avoid it. <em>It doesn’t
have to be this way</em>. We can have our cake and eat it, too: promptly
delivering lines to a terminal at fully-buffered speeds.</p>

<p>For example, Info-ZIP <code>unzip</code> prints file names as it extract them from an
archive. This printout dominates extraction time of archives with many
small files. Sometimes I run <code>unzip</code> without options, then my terminal
floods with file names and I know that my extraction is bottlenecked. So I
cancel and restart with <code>-q</code> (quiet) to avoid the bottleneck, because it’s
faster to start over than wait. Conscious of this experience, I avoided
the problem in <a href="/blog/2026/10/13/">tugz</a> <code>unzip</code> without changing the user interface. You
get a listing but you don’t have to pay for it.</p>

<pre class="highlight"><code>$ unzip example.zip
Archive:  example.zip
  inflating: example.bmp
  inflating: example.svg
  inflating: example.txt
</code></pre>

<p>Consider this C program that prints three lines:</p>

<pre class="highlight"><code><span class="p">#include</span> <span class="s">&lt;stdio.h&gt;</span>

<span class="t">int</span> <span class="f">main</span>()
{
    puts(<span class="s">"a\nb"</span>);
    puts(<span class="s">"c"</span>);
}
</code></pre>

<p>If we examine the writes with, say, <a href="/blog/2018/06/23/">strace</a>:</p>

<pre class="highlight"><code>$ strace -o log -e write ./a.out
a
b
c
</code></pre>

<p>We might see (here glibc):</p>

<pre class="highlight"><code><span class="f">write</span>(<span class="m">1</span>, <span class="s">"a\n"</span>, <span class="m">2</span>)                      = <span class="m">2</span>
<span class="f">write</span>(<span class="m">1</span>, <span class="s">"b\n"</span>, <span class="m">2</span>)                      = <span class="m">2</span>
<span class="f">write</span>(<span class="m">1</span>, <span class="s">"c\n"</span>, <span class="m">2</span>)                      = <span class="m">2</span>
</code></pre>

<p>Yes, glibc takes line buffering <em>quite literally</em>, even splitting writes
on a string. Some implementations are a little more sophisticated:</p>

<pre class="highlight"><code><span class="f">write</span>(<span class="m">1</span>, <span class="s">"a\nb\n"</span>, <span class="m">2</span>)                   = <span class="m">4</span>
<span class="f">write</span>(<span class="m">1</span>, <span class="s">"c\n"</span>, <span class="m">2</span>)                      = <span class="m">2</span>
</code></pre>

<p>CPython makes exactly those writes for this program:</p>

<pre class="highlight"><code><span class="k">import</span> sys

sys.stdout.write(<span class="s">"a\nb\n"</span>)
sys.stdout.write(<span class="s">"c\n"</span>)
</code></pre>

<p>Its rule is to flush if its buffer contains a newline after appending the
new output, with the consequence of possibly outputting partial lines.
Building on <a href="/blog/2023/02/13/">the previous example</a>:</p>

<pre class="highlight"><code><span class="k">enum</span> {
    BUF_ERR  = <span class="m">1</span> &lt;&lt; <span class="m">0</span>,
    BUF_LINE = <span class="m">1</span> &lt;&lt; <span class="m">1</span>,
};

<span class="k">typedef</span> <span class="k">struct</span> {
    <span class="t">uint8_t</span>  *buf;
    <span class="t">ptrdiff_t</span> cap;
    <span class="t">ptrdiff_t</span> len;
    <span class="t">int</span>       fd;
    <span class="t">int</span>       flags;
} <span class="t">Buf</span>;

<span class="t">bool</span> <span class="f">os_write</span>(<span class="t">int</span> fd, <span class="t">void</span> *, <span class="t">ptrdiff_t</span>);

<span class="t">void</span> <span class="f">flush</span>(<span class="t">Buf</span> *b)
{
    <span class="k">if</span> (!(b-&gt;flags &amp; BUF_ERR) &amp;&amp; b-&gt;len) {
        <span class="k">if</span> (!os_write(b-&gt;fd, b-&gt;buf, b-&gt;len)) {
            b-&gt;flags |= BUF_ERR;
        }
        b-&gt;len = <span class="m">0</span>;
    }
}
</code></pre>

<p>Such line buffering might be achieved with a test-then-flush:</p>

<pre class="highlight"><code><span class="t">bool</span> <span class="f">has_newline</span>(<span class="t">Buf</span> *b)
{
    <span class="t">int</span> r = <span class="m">0</span>;
    <span class="k">for</span> (<span class="t">ptrdiff_t</span> i = <span class="m">0</span>; i &lt; b-&gt;len; i++) {
        r |= b-&gt;buf[i] == <span class="s">'\n'</span>;
    }
    <span class="k">return</span> r;
}

<span class="t">void</span> <span class="f">append</span>(<span class="t">Buf</span> *b, <span class="t">uint8_t</span> *src, <span class="t">ptrdiff_t</span> len)
{
    <span class="t">uint8_t</span> *end = src + len;

    <span class="k">while</span> (!(b-&gt;flags &amp; BUF_ERR) &amp;&amp; src&lt;end) {
        <span class="t">ptrdiff_t</span> left   = end - src;
        <span class="t">ptrdiff_t</span> avail  = b-&gt;cap - b-&gt;len;
        <span class="t">ptrdiff_t</span> amount = avail&lt;left ? avail : left;

        memcpy(b-&gt;buf+b-&gt;len, src, amount);
        b-&gt;len += amount;
        src += amount;

        <span class="k">if</span> (amount &lt; left) {
            flush(b);  <span class="c">// make room</span>
        }
    }

    <span class="c">// test-then-flush if line buffered</span>
    <span class="k">if</span> ((b-&gt;flags &amp; BUF_LINE) &amp;&amp; has_newline(b)) {
        flush(b);
    }
}
</code></pre>

<p>If we’ve got a bunch of lines to print and append them one after another
into the buffer, that’s a lot of writes which could have been one.</p>

<h3 id="eat-all-the-cake">Eat all the cake</h3>

<p>Let’s think about our goal: If the output is a terminal then we don’t want
to hold back lines indefinitely. That’s not the same as a write on every
newline. There’s already a solution for the problem where we’re in a hurry
to send out data, but the underlying, <em>buffer-oblivious</em> caller makes lots
of small writes: <a href="https://en.wikipedia.org/wiki/Nagle%27s_algorithm">Nagle’s algorithm</a>.</p>

<p>That is, <strong>flush the buffer if it’s been idle a short time</strong>. We don’t
actually care about newlines in particular. The catch is that it requires
tweaking the interface. Add a deadline to the buffer:</p>

<pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> {
    <span class="t">int64_t</span>   deadline;
    <span class="c">// ...</span>
} <span class="t">Buf</span>;

<span class="t">int64_t</span> <span class="f">os_monotonic_clock</span>();

<span class="t">void</span> <span class="f">deadline_flush</span>(<span class="t">Buf</span> *b)
{
    <span class="k">if</span> ((b-&gt;flags &amp; BUF_LINE) &amp;&amp; b-&gt;len) {
        <span class="t">int64_t</span> now = os_monotonic_clock();
        <span class="k">if</span> (b-&gt;deadline &amp;&amp; now&gt;b-&gt;deadline) {
            flush(b);
            b-&gt;deadline = <span class="m">0</span>;
        } <span class="k">else</span> {
            b-&gt;deadline = now + DEADLINE;
        }
    }
}
</code></pre>

<p>Where <code>DEADLINE</code> is something appropriate for human perception, like 100ms
or so. We could poke this concurrently in a thread, awoken via futex when
data is written to the buffer. But better to poke it between short stints
of work, which is how tugz works. For example (using <a href="/blog/2025/01/19/#strings">my strings</a>):</p>

<pre class="highlight"><code>    <span class="k">for</span> (<span class="c">/* filename in filenames */</span>) {
        <span class="t">Str</span> pre = S(<span class="s">"  inflating: "</span>);
        <span class="t">Str</span> suf = S(<span class="s">"\n"</span>);
        append(&amp;stdout, pre.data, pre.len);
        append(&amp;stdout, filename.data, filename.len);
        append(&amp;stdout, suf.data, suf.len);

        <span class="k">while</span> (<span class="c">/* not done inflating */</span>) {
            <span class="c">// 1. inflate block</span>
            <span class="c">// 2. write block</span>
            deadline_flush(&amp;stdout);
        }
    }
</code></pre>

<p>If it processes multiple files within the deadline, their names write out
together. No threads. The behavior difference compared to traditional
flush-on-newlines is imperceptible to a humans, aside from the fact their
program runs faster. One more reason not to bother with <a href="/blog/2023/02/11/">crummy C
stdio</a>.</p>

]]>
    </content>
  </entry>
  <entry>
    <title>Prospero: a new coding harness for a more civilized age</title>
    <link rel="alternate" type="text/html" href="https://nullprogram.com/blog/2026/10/31/"/>
    <id>urn:uuid:6d69f504-f96a-8e26-8a48-b4aa367ee034</id>
    <updated>2026-10-31T03:38:07Z</updated>
    <category term="ai"/>
    <content type="html">
      <![CDATA[<p>I’ve been <a href="/blog/2024/11/10/">fascinated with local LLMs</a> for a couple of years now.
The better open weight models, including some that inference on regular,
higher-end consumer hardware at tolerable speeds, are now reliable enough
to handle simpler programming tasks. This requires a coding harness, but
so far this year I’ve been disappointed by all the harness offerings. For
inexplicable reasons most are built with web stacks, and consume enormous
amounts of memory. At its core a coding harness is essentially curl glued
to <code>popen()</code>. This should not require gigabytes of RAM. So I built my own:
<strong><a href="https://github.com/skeeto/prospero">Prospero</a></strong>. It’s written in <a href="/blog/2026/10/04/">my C++ style</a> with a snappy <a href="/blog/2026/04/07/">Dear
ImGui docking interface</a>. It uses around two orders of magnitude
fewer resources than other harnesses without sacrificing features.</p>

<p><img src="/img/prospero/screenshot.png" alt="" /></p>

<p>Dark mode aficionados are covered, too:</p>

<p><img src="/img/prospero/screenshot-dark.png" alt="" /></p>

<p>Note that Prospero comes with no model providers built-in. You’re expected
to supply that yourself, such as with <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a>, a workplace on-prem
model, OpenRouter, Anthropic API, OpenAI API, etc. (You can’t use your
Claude subscription.)</p>

<p>For those unfamiliar with coding harnesses: In a chat interface you and
the AI take turns writing messages to each other. A coding harness extends
this concept, allowing an AI to “talk” to tools offered by the harness.
Tools “reply” with their outputs. A tool might read or write a file, or
run a shell command. An AI will go from tool call to tool call on its own,
looping on the results. Such an AI is called an <em>agent</em>. Occasionally the
AI will send you a message to inform you of progress or to ask a question,
and the chat interface is still there.</p>

<p>Some harnesses, including Prospero, allow agents to spawn <em>subagents</em> to
do their bidding, to work in parallel or keep their <em>context</em> clean. That
is, to not fill their working memory with subtasks. Agents can message one
another and coordinate on tasks. Given SSH access to another machine, or
over plain WSL, Prospero can deploy a headless copy of itself to other
hosts so that agents can operate on those systems. The <a href="https://skeeto.itch.io/prospero">Professional
Edition</a> embeds engines for combinations of x86/x86-64 and ARM64 on
macOS, Windows, and Linux. The Windows x86 engine even works on Windows
XP, and can play solitaire and minesweeper if you ask.</p>

<p>The last is because Prospero is one of the few harnesses with <em>desktop
control</em> capabilities. If your model supports vision — any any model worth
using does — then it can “see” your desktop and interact with it. It can
click, type, and drag to test and debug GUI applications live without a
specialized, per-app <a href="/blog/2026/09/17/">MCP assist</a>.</p>

<p>This headless engine is why no TUI is necessary. The harness engine runs
on the headless system, and the UI runs on your desktop or laptop. With a
little extra setup, the harness to run fully independently of the GUI, so
that it will continue without the GUI system attached — re-attachable
later.</p>

<p>Because Prospero is so lean, it may be the only harness that can drive a
<a href="/blog/2025/02/05/">fuzz campaign</a>, particularly on a headless system, without the
Linux OOM killer targeting <em>the memory-gobbling harness</em> rather than the
out-of-control fuzz target that triggered it. So many times I’ve checked
back in on a fuzz campaign hours later, to find nothing was accomplished
because the harness process is gone.</p>

<p>(Why have an agent run a fuzz campaign? Because it can fix discovered bugs
on the fly and keep the campaign going! It’s a no brainer.)</p>

<h3 id="configuration">Configuration</h3>

<p>The configuration lives at <code>~/.config/prospero/prospero.ini</code>. Easy to
edit. No JSON, thank goodness. Check it into source control, copy-paste
it. All the usual benefits. Alternatively, the Settings page can reach
every setting from the GUI, so you don’t have to edit the INI by hand. INI
files can also include each other, not just naming file system paths but
URLs:</p>

<pre class="highlight"><code><span class="v">include</span> = https://example.com/prospero.ini
</code></pre>

<p>The idea is to allow a live shared or managed configuration. Includes by
URL are restricted in what they can set, with a hard rule against anything
which might appear in shell command. Some trust is still required, as a
configuration can extract an environment variable by using it as an API
key and supplying an endpoint it controls.</p>

<p>Prospero fetches a model list and other parameters like context size from
the endpoint, but you might want to tweak it. A typical configuration may
look like:</p>

<pre class="highlight"><code><span class="v">model</span> = falstaff/Qwen3.8-27B

<span class="k">[endpoint falstaff]</span>
<span class="v">url</span> = http://falstaff.local/
<span class="v">key</span> = env:FALSTAFF_API_KEY

<span class="k">[model Qwen3.8-27B]</span>
<span class="v">edit_format</span> = replace
<span class="v">template.reasoning_effort</span> = medium

<span class="k">[model Qwen3.8-27B-xhigh]</span>
<span class="v">like</span> = Qwen3.8-27B
<span class="v">template.reasoning_effort</span> = xhigh

<span class="k">[web]</span>
<span class="v">search</span> = searxng
<span class="v">url</span> = https://search.example.com/
<span class="v">key</span> = env:EXAMPLE_API_KEY
</code></pre>

<h3 id="passive-rendering">Passive rendering</h3>

<p>The ImGui paradigm typically uses <em>active rendering</em>, redrawing on vsync
like a video game. This is appropriate in some contexts, like interfaces
within video games. Less so for mostly-static text-oriented UIs, such as a
coding harness. Instead Prospero uses <em>passive rendering</em> with <em>settle
frames</em> to finish animations before going idle. When there’s nothing to
do, Prospero does nothing. It won’t chew on the GPU while the agent is
waiting on a build.</p>

<h3 id="never-wait-forever">Never wait forever</h3>

<p>An annoyance with most other harnesses is how easily they get stuck. I set
a long-running task, go to sleep, and in the morning find the harness hung
on a trivial issue two hours into the night, wasting the rest. Therefore
Prospero prime design principle: <em>Never wait forever</em>. <strong>Every operation
has a deadline.</strong> This decision influences everything. If you’re not there
to grant a requested permission, the agent will get a denial by timeout
and must do without. To assist, Prospero will raise a system toast when an
agent requires your attention so that you’re less likely to miss it if
you’re attending.</p>

<p>The harness itself <em>plows through intermittent errors</em>, trying its best to
drive the agent to complete its task. All network requests have timeouts,
and watchdogs prevent <a href="/blog/2019/03/22/">endless waiting</a>. Agent mailboxes <a href="https://lucumr.pocoo.org/2020/1/1/async-pressure/">are
bounded</a>.</p>

<p>Shell commands that hit their timeout convert to asynchronous background
commands. The agent can continue to wait on them, with a timeout, and so
will be woken at regular intervals to decide if it should continue.</p>

<p>The one exception is Plan mode, where it waits indefinitely after offering
a plan or requesting clarification (<code>ask_user</code> tool). It’s expected that a
user is attending to the agent during Plan mode.</p>

<h3 id="no-stray-processes">No stray processes</h3>

<p>The harnesses I’ve used have improved, but another regular annoyance
earlier this year was agents starting processes that never get cleaned up.
An agent kicks off a parallel build, hits a timeout because it was too
optimistic, the build “stops” by killing a piece of the build system. But
actually <em>the build continues anyway</em>, at least in part. The agent tries
again, and now two builds are going, interfering with each other, making a
mess and bringing my system to its knees.</p>

<p>For awhile I was wary of letting agents run builds in certain harnesses,
which is a terrible outcome. <em>You want agents to close the loop</em> and
iterate independently. A human in the loop is like putting a ball and
chain on the agent, giving up most of its advantages.</p>

<p>Prospero promoting a synchronous timeout to an asynchronous task is a huge
improvement on its own, but there’s an additional principle at play: The
whole process tree lives and dies together. When a command is stopped, <em>it
stops</em>. This is <a href="https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects">trivial on Windows</a>, though few harnesses seem to
take advantage of it. It’s more difficult on macOS and Linux, but Prospero
follows through.</p>

<h3 id="built-in-self-help">Built-in self-help</h3>

<p>Like every harness, Prospero supports <em>skills</em>, a pre-packaged set of
instructions on how to accomplish some task (“I know kung fu.”). Prospero
contains a skill on itself and can read its built-in documentation. Ask
the agent how to do or configure something in Prospero, and the agent can
only answers your question, it can provide a hyperlink taking you to the
relevant place in the UI. It can also edit your <code>prospero.ini</code> and reload
it, to configure things on your behalf. This is not unique to Prospero,
but I’m not aware of any harness doing this nearly as well.</p>

<h3 id="choose-your-shell">Choose your shell</h3>

<p>On Windows, Prospero is designed to work well with <a href="https://github.com/skeeto/w64devkit">w64devkit</a>, and will
prefer its shell if available. Models designed for coding know w64devkit
out-of-the-box, so these tools are complementary. Of course you’re not
locked into that shell, and you can freely choose a different default
shell. But Prospero is a rare harness where you can change shells mid
session. Sometimes PowerShell is more suited to the task, so you can pick
it out of the dropdown. The agent is informed of the change, and told
about known quirks (i.e. “this is an old Bash!”). On macOS you can switch
between bash and zsh.</p>

<h3 id="the-usual-features">The usual features</h3>

<p>Prospero has the usual sorts of features you can expect from an advanced
shell. It can use Git worktrees, including subagent worktrees, so that
agents can share a repository. It speaks the Model Context Protocol (MCP).
It can search the web (if you allow). You can start side chats that share
context, realized as subagents. You can fork sessions.</p>

<p>For now there is no built-in terminal or editor. Instead Prospero will
open an external terminal or editor of your choosing. I’d rather use Vim
than a built-in notepad.exe-equivalent. With Ctrl+G you can edit your
prompt in an external editor.</p>

<p>Like all harnesses, the permissions system will not stop a malicious
agent. Agents are writing and running programs on your computer, and so
can ultimately access everything the harness can access. Permissions are
about gently reminding the agent of your wishes, and catching simple
mistakes. More like, “Whoa there, stay within the project” or, “Do not
control my desktop at this time.” If you want to lock an agent out, you
have to restrict the harness itself, putting it in a container or on a
Raspberry Pi, at the usual cost of inconvenience or cutting off useful
resources.</p>

<h3 id="community-edition-versus-professional">Community Edition versus Professional</h3>

<p>I’m really excited about Prospero, and <strong>I honestly feel it’s one of the
best coding harnesses available</strong>. It’s how such tools ought to be built,
but until now nobody else stepped up to do it. It would take a video to do
it justice, and maybe I’ll do that sometime, but that’s currently outside
my experience.</p>

<p>Most of the audience for my projects are hobbyists, even w64devkit. But if
Prospero goes beyond me and my little circle, I expect most users would be
professionals building commercial products. I’ve also invested more than
just my time into this project — already true for w64devkit, even more
true with Prospero. So while it’s an open source project, I’ve split it
into two tiers:</p>

<ul>
  <li>
    <p>Community Edition: the build you find on GitHub at no cost. It has no
built-in headless engines, so remote deployments are limited. Plus a
little nag text to remind you you’re on the Community Edition. Suited
for hobbyists.</p>
  </li>
  <li>
    <p><a href="https://skeeto.itch.io/prospero">Professional Edition</a>: Pay a nominal fee, get a DRM-free download
with free updates. All the headless engines are built in so you can push
remote deployments and remote sessions on other systems at the press of
a button. A serious feature for professionals.</p>
  </li>
</ul>

<p>Windows and macOS builds are code-signed in both cases (some of the cost).
If Prospero is useful for you professionally, even without the remote
deployment feature, you’re still encouraged to buy a copy to support my
ongoing costs!</p>

]]>
    </content>
  </entry>
  <entry>
    <title>tugz: a new deflate library with three applications</title>
    <link rel="alternate" type="text/html" href="https://nullprogram.com/blog/2026/10/13/"/>
    <id>urn:uuid:21156575-515c-862e-9b0f-8a86d5b229fc</id>
    <updated>2026-10-13T22:32:24Z</updated>
    
    <content type="html">
      <![CDATA[<p>I spent a week creating a new <a href="https://en.wikipedia.org/wiki/Deflate">deflate</a> implementation from scratch in C
named <strong><a href="https://github.com/skeeto/tugz">tugz</a></strong> to provide an <a href="https://infozip.sourceforge.net/">Info-ZIP</a>-compatible <code>zip</code> program for
<a href="https://github.com/skeeto/w64devkit">w64devkit</a> 2.10.1 (released today). I also replaced <a href="https://frippery.org/busybox/">busybox-w32</a>’s
deflate-based <code>unzip</code>, <code>gzip</code>, <code>gunzip</code>, and <code>zcat</code> apps as well. My new
deflate performs much better than <a href="https://zlib.net/">zlib</a> and on par with <a href="https://github.com/ebiggers/libdeflate">libdeflate</a>.
In general the new programs are substantially faster and produce slightly
smaller outputs than the BusyBox equivalents, which is optimized for size
than speed after all, but also compared to the original gzip and Info-ZIP
you get in a typical Linux distribution. The tugz programs are
<a href="/blog/2023/02/15/">CRT-free</a> (inspect them with <a href="/blog/2024/06/30/">peports</a>!) with first-class Windows
support and better Unicode handling.</p>

<p><img src="/img/tugz.png" alt="" /></p>

<p>Besides the cute logo, tugz is public domain and written entirely in my
personal style with unity builds, <a href="/blog/2025/01/19/">arena allocation</a>, platforms layers
with minimal interfacing (read, write, open, etc.), and extensive <a href="/blog/2025/02/05/">fuzz
testing</a>. The <code>zip</code> implementation carefully avoids a Windows <a href="https://github.com/ebiggers/libdeflate/issues/323">deflate
bug</a> that has personally bitten me. Its CMake build drives the more
complex machinery (tests, etc.), but it’s optional for straightforward
builds. Just compile the platform layers. For instance, in w64devkit:</p>

<pre class="highlight"><code>$ cc -nostartfiles -O2 -o  zip  platform/zip_windows.c    -lmemory
$ cc -nostartfiles -O2 -o gzip  platform/gzip_windows.c   -lmemory
$ cc -nostartfiles -O2 -o unzip platform/unzip_windows.c  -lmemory
$ cc -shared -nostdlib -O2 -o tugz.dll platform/libtugz.c -lmemory
</code></pre>

<p><code>gunzip</code> and <code>zcat</code> are aliases/symlinks for <code>gzip</code>. <code>tugz.h</code> defines the
library interface, featuring an <a href="/blog/2023/12/17/">allocator callback</a>. I’m not currently
using the library in any projects, nor is it included in w64devkit, but it
might come in handy someday. The applications use the internal interfaces.</p>

<p>tugz <code>unzip</code> supports Deflate64 decompression, an extension to deflate
with a larger sliding window. Windows built-in ZIP support uses it for ZIP
files larger than 2GiB, to little practical benefit. BusyBox <code>unzip</code> does
not support Deflate64 and so cannot unzip some Windows-made ZIP files. The
replacement will work better in these cases.</p>

<p>Unlike <a href="https://github.com/skeeto/u-config/blob/master/src/linux_noarch.c">u-config</a>, I did not write Linux syscall platform layers this
time, but it would not be difficult to do so. Then such builds would truly
work in any Linux distribution, new or old. I leave it as a fun exercise
for the reader.</p>

<p>The original gzip and Info-ZIP are old, and like so many tools from that
era they include line conversion features. When you decompress files you
can translate the line endings at the same time in one tool. Over the
years, archaic line-ending features have caused far more harm than good
(<em>arg</em>… Git <code>core.autocrlf</code>), so I deliberately omit them. Using these
flags is an error.</p>

<p>Similarly, ZIP encryption features are deliberately left unimplemented.
Attempting to use encryption is an error.</p>

<p>One of my hard and fast rules was full support for Windows XP. This wasn’t
automatic, but nor was it difficult. tugz needs several <code>*ByHandle{Ex,}W</code>
functions from Win32 that are unavailable in XP. When they’re unavailable,
it falls back to equivalent Nt functions via GetProcAddress. I also made a
<a href="https://github.com/skeeto/sshd">lightwight SSH server</a> that is trivial to build and run on Windows
XP, allowing coding agents to automatically test and verify tugz on my old
systems. It’s quite fascinating to watch it take control of my old laptop,
and it’s already proving useful in other projects, including w64devkit.</p>

<p><strong>Update</strong>: After <a href="https://github.com/skeeto/tugz/issues/1">a request for <code>zipinfo</code> / <code>unzip -Z</code></a>, tugz
now has this utility as well. It will appear in the next w64devkit. It
also gained a minimal, classic Win32 GUI:</p>

<p><img src="/img/tugz-gui.png" alt="" /></p>

<h3 id="usage-in-w64devkit">Usage in w64devkit</h3>

<p>What does it mean for users? You can invoke <code>zip</code> in your build scripts or
whatever to package releases in a Windows-friendly ZIP file. Before today
the only option in w64devkit was going through <a href="https://cmake.org/cmake/help/latest/module/CPack.html">CPack</a>. A <a href="/blog/2017/08/20/">Makefile</a>,
perhaps:</p>

<pre class="highlight"><code>myapp-$VERSION.zip: myapp.exe README.md
    rm -f $@
    zip -X9 $@ $^
</code></pre>

<p>The <code>rm</code> because tugz follows the Info-ZIP behaviors of update-in-place.
The output is deterministic, particularly when given <code>-X</code> (omit extended
attributes) so that your releases are stable. The <code>-9</code> requests maximum
compression. Or maybe after you’ve <a href="/blog/2026/10/04/">generated my blog</a>, recursively
zip it up for archival:</p>

<pre class="highlight"><code>$ ssg
$ zip -r nullprogram.zip _site/
</code></pre>

<p>This is another project I’d been considering for a couple years now. It
turned out better than I expected, and I’m quite excited about it. In my
eyes it represents a kind of ideal project, and I wish more projects were
shaped like this.</p>

]]>
    </content>
  </entry>
  <entry>
    <title>A new, bespoke static site generator to replace Jekyll</title>
    <link rel="alternate" type="text/html" href="https://nullprogram.com/blog/2026/10/04/"/>
    <id>urn:uuid:bee8d4c0-5185-8d4b-b14a-4aba71276e87</id>
    <updated>2026-10-04T16:35:49Z</updated>
    <category term="c"/><category term="cpp"/><category term="meta"/><category term="ai"/>
    <content type="html">
      <![CDATA[<p>My blog began as a <a href="https://www.blosxom.com/">blosxom</a> (Perl) site running on a VPS. In 2011 I
<a href="/blog/2011/08/05/">moved to the new GitHub Pages</a>, with the site generated statically
by <a href="https://jekyllrb.com/">Jekyll</a> (Ruby) on a GitHub server. It was a no-brainer: easier,
faster, and cheaper, better in every way. After 15 years of Jekyll, and
spurred by <a href="https://napcakes.nekoweb.org/posts/m4-is-nice/">a post on m4</a>, this week I replaced it with <a href="https://github.com/skeeto/skeeto.github.com/tree/master/_ssg">a new,
custom-built static site generator</a>, dubbed <em>ssg</em>, in “C with
templates” C++20. The ~8KLoC source <a href="/blog/2025/01/19/">closely follows my personal coding
style</a> including <a href="/blog/2024/04/14/">templated arenas and slices</a>, zero
dependencies, and a libc-free core. It’s wicked fast, and a complete, cold
generation of my blog takes 150ms on my MacBook. That is, <em>it’s done
before Ruby would even reach Jekyll’s entry point</em>. It’s a been a great,
real-world demonstration of the effectiveness of my coding philosophy.</p>

<p>Look around and you’ll find almost nothing visually changed. Outside of
syntax highlighting, the HTML is semantically identical. I did not try to
match Rouge’s syntax highlighting, just its CSS selectors so that I could
use my style sheet unmodified. With that in my own hands, I now get syntax
highlighting that better suits my needs, some Rouge bugs gone and support
for new languages, particularly assembly (<a href="/blog/2025/04/04/">WAT</a>, <a href="/blog/2024/02/05/">AT&amp;T</a>, <a href="/blog/2022/02/18/">Aarch64</a>)
and <a href="/blog/2020/11/17/">QBasic</a>.</p>

<p>Because ssg only supports my site, and its source lives along side it, I
don’t need a template system, i.e. Liquid. I can modify the C++ source as
needed. So the upgrade was three steps: (1) fix 19 years of accumulated
site bugs that Jekyll quietly tolerated, (2) add the new generator, (3)
delete Liquid templates and pre-generated tag pages (now dynamically
generated, with preservation of original feed UUIDs). Jekyll and ssg both
work simultaneously at step #2, allowing side-by-side comparisons from the
same site source. This is where most of the time was spent. Dropping
templates means performance comparisons with Jekyll are unfair, as Jekyll
is solving a different problem. (But I think it’s fair to speculate that
ssg with Liquid templates would still smoke Jekyll!)</p>

<p>Historically, Jekyll was a dreadfully slow site generator until <a href="https://jekyllrb.com/news/2019/08/20/jekyll-4-0-0-released/">the 4.0.0
release in August 2019</a>. While performance is mostly resolved, I still
have ingrained habits to work around it. The final Jekyll version of my
site took ~2 seconds on the same MacBook. Not too bad, and it has a fast
<code>--incremental</code> mode, though it’s always been a little buggy, not quite
matching a full generation.</p>

<p><strong>No, the pain point for Jekyll is not performance but <em>deployment</em></strong>.
Especially when trying to match the GitHub Pages configuration. The Ruby
deployment situation remains just awful. I never once generated my blog on
Windows, in part to avoid jumping through hoops to set up Jekyll. Last
year I switched to macOS (from Linux) as my primary, personal development
environment. This meant setting up Jekyll on macOS, and <a href="https://jekyllrb.com/docs/installation/macos/">the situation is
farcical</a>. The Ruby community ought to feel embarrassed about this.
The <code>source</code> lines delay shell startup by 60ms, and I’m unwilling to bear
that cost in nearly every shell just to occasionally run Jekyll, plus
environment litter that may interfere with other tools. So I needed to
remember to enter an isolated Jekyll environment, and to tell AIs to use
if they needed Jekyll.</p>

<p>With ssg you just need a C++ compiler. As native application, you don’t
need any development tools once it’s built of course, and the compiled
program is a single file that could be copied to another system and run
as-is. It’s a unity build — I did say it closely follows my personal style
— so you don’t even need a build system. On <a href="https://github.com/skeeto/w64devkit">w64devkit</a> that looks like:</p>

<pre class="highlight"><code>$ cc -nostartfiles -o _ssg/ssg.exe _ssg/main_win32.cpp
</code></pre>

<p><code>cc</code> (or <code>gcc</code>) works because the compiler driver knows to invoke the C++
front-end for this input. It doesn’t use the C++ standard library, so the
linker doesn’t need <code>-lstdc++</code>. <code>-O0</code> builds like this run at about half
speed compared to optimized builds, and <code>-O1</code> is sufficient to get all the
compiler optimization benefits. Normally debug builds would be around 10x
slower, but fast debug builds is par for the course for my style. While
you don’t need it, there <em>is</em> a CMake build, but that’s really just for
driving the test suite.</p>

<p>Both build and tool work on Windows XP, too, so for the first time ever I
could <a href="/blog/2025/03/02/">fully compose a post on my old XP laptop</a> if I wished.</p>

<p>On the MacBook this <code>-O0</code> build takes ~150ms — quite good for 8,372 lines
of code. A cold site generation takes this build ~260ms. That’s so fast I
could very well re-compile the generator each time I regenerate the site.
Indeed that’s exactly how it works in the GitHub Actions pipeline. Overall
it’s faster to use a debug build than a release build. The cache is too
slow and unreliable for release builds to make sense in Actions.</p>

<pre class="highlight"><code>$ cc -std=c++20 -o _ssg/ssg _ssg/main_posix.cpp  # macOS
</code></pre>

<p>For the first decade of GitHub Pages, Jekyll was the only option. It ran
opaquely in the somewhere at GitHub with a pass/fail result, requiring a
local matching configuration for debugging. When they introduced GitHub
Actions in 2018, Jekyll was a pre-configured, transparent pipeline, and it
became possible to run whatever generator you wanted in its place. I’m
finally taking advantage of that, and I still get automatic page builds on
push like I had with Jekyll. It’s now slightly faster — Actions overhead
dominates either way — despite compiling an entire C++ program each run.</p>

<p>I do not plan to divorce my generator from my blog, so ssg will not become
a general-purpose static site generator. Its whole purpose is to serve
this one particular need. You’re free to fork it and use it as a basis for
your own needs, of course. This sort of bespoke, written-to-order software
is likely the future of software. Why bother with one-site-fits-all when
an exactly-sized solution has the same cost?</p>

<h3 id="a-first-for-generative-ai">A first for generative AI</h3>

<p>I’ve been thinking about this project for years. Had I written it in the
2023–2025 time frame, I estimate ~2–4 weeks, using <a href="/blog/2023/01/18/">u-config</a> (3KLoC,
lower complexity) as a measuring stick. Here in <a href="/blog/2026/03/29/">2026</a>, it took about
~20 minutes to write my detailed prompt, then ~2 hours for Opus 5.5 in
Claude Desktop to produce ssg in essentially its current form, bug-free as
far as I can tell, including an <em>untested</em> Win32 platform layer (written
on the MacBook, no Wine). I’ve spent more time on this article than I did
on the new generator.</p>

<p>I spent a couple more hours looking it over, shocked at Opus 5.5 perfectly
matching my personal style. It really does look like I wrote ssg. Reading
it is uncanny, like seeing someone reproduce my own handwriting such that
I couldn’t distinguish it. During this review I realized we ought to use
debug builds in the pipeline, so we had <a href="https://github.com/skeeto/skeeto.github.com/commit/2c9aa4e9">one follow-up on the hot spot in
debug builds</a>. Then I noticed the Win32 platform layer generated an
empty site on Windows XP, <a href="https://github.com/skeeto/skeeto.github.com/commit/35228acf">another small followup</a>. (XP wasn’t in
my original prompt, so I don’t count this as a bug.) That was it.</p>

<p>Two years ago <a href="/blog/2024/11/10/">I said</a> that AI-generated code was too poor to be
practical. That changed by the end of 2025. In earlier 2026 projects I
tried to get AI to write C following my style, using my writing on the
subject as a guide, but they’d just recreate the <a href="/blog/2023/02/11/">no-good standard
library</a> from scratch then flub arena allocation. Ask those models
to write conventional C and you get the usual, error-prone C you’ll find
anywhere. So I settled on conventional C++ as Good Enough. I could
complete projects ~20x faster, but with results not quite as good as I’d
have done myself. A reasonable trade-off.</p>

<p>Opus 5.5 released on September 22nd. It is the first model I’ve used that
actually understands my coding philosophy and <strong>writes C at least as well
as me</strong>. This was the ideal project for this test, as my core writing on
this subject was naturally in context, but it generalizes when referencing
my writing and prior work. As of two weeks ago I can now have my cake and
eat it, too. No more trade-offs.</p>

<p>If you’d like to get similar C-with-templates results on your own project,
cite these four articles in your prompt:</p>

<ul>
  <li><a href="https://nullprogram.com/blog/2025/01/19/">https://nullprogram.com/blog/2025/01/19/</a></li>
  <li><a href="https://nullprogram.com/blog/2024/04/14/">https://nullprogram.com/blog/2024/04/14/</a></li>
  <li><a href="https://nullprogram.com/blog/2025/09/30/">https://nullprogram.com/blog/2025/09/30/</a></li>
  <li><a href="https://nullprogram.com/blog/2025/10/16/">https://nullprogram.com/blog/2025/10/16/</a></li>
</ul>

<p>As well as perhaps my relevant, similar projects. All the frontier models
know about me personally and are familiar with my work, so invoking my
name may be enough. If the model is as good as Opus 5.5, you might get an
accurate impression of how I would have done your project.</p>

]]>
    </content>
  </entry>
  <entry>
    <title>What's been going on in w64devkit the past year</title>
    <link rel="alternate" type="text/html" href="https://nullprogram.com/blog/2026/09/20/"/>
    <id>urn:uuid:843e3ff4-8bd3-4106-bca9-608a80e66545</id>
    <updated>2026-09-20T01:49:36Z</updated>
    <category term="c"/><category term="cpp"/>
    <content type="html">
      <![CDATA[<p>The past year has been exciting for <a href="https://github.com/skeeto/w64devkit">w64devkit</a>, which like any software
distribution is never complete. <a href="https://peter0x44.github.io/">Peter0x44</a> joined as co-maintainer, and
has pushed the project in good, new directions, with ideas I would never
have considered. Many of his improvements have gone back upstream, and so
you may benefit even if you don’t use w64devkit. I’d like to touch on the
various odds and ends from the past year.</p>

<h3 id="release-security">Release security</h3>

<p>In April I <a href="/blog/2026/04/25/">announced that release packaging is now signed</a>. Today
<em>all</em> EXEs and DLLS in w64devkit are code-signed with my key. My signing
key established a good reputation thanks to thousands of unique signatures
observed on the order of ~100,000 hosts — a pleasant side effect from
including ~300 binaries in a release. Users should have fewer problems
these days with security software. MSYS2 has also <a href="https://www.msys2.org/dev/code-signing/">adopted my signing
tool</a>, <code>aas-sign</code>, which is now included in w64devkit releases.</p>

<p>Builds are now automated by GitHub Actions, triggered when I (and <em>only</em>
I) push a new tag. That process code-signs and creates the release. Every
step of the release process is transparent, derived strictly from source
in the repository. Nowhere does it go behind a curtain and permit secret
tampering.</p>

<p>But that’s not all. I enabled <em>release immutability</em>: On publish, release
artifacts are locked in and nobody, not even me, can modify them. You can
tell by the presence of a release attestation at the end of the artifacts
listing. Nobody involved in the project down the road can go rogue and
sneak something into an old release.</p>

<h3 id="toolchain-changes">Toolchain changes</h3>

<p>The x64 release is now a “multilib” toolchain. That is it can compile
programs for 32-bit Windows, like a superset of the x86 release. The only
purpose of the x86 release is to run w64devkit on older hardware or older
operating systems. Like the x86 release, it targets Windows XP by default
and requires a CPU supporting SSE2 (i.e. at least Pentium 4).</p>

<p>To target x86, pass <code>-m32</code> when compiling and linking. Or better, use
tools prefixed with the <code>i686-w64-mingw32</code> architecture triple. The latter
is easier in general because different tools require different switches,
and the prefixed tools are <a href="/blog/2021/02/08/">aliases</a> that automatically do the right
thing.</p>

<pre class="highlight"><code>$ x86_64-w64-mingw32-gcc -o hello64.exe hello.c
$ i686-w64-mingw32-gcc -o hello32.exe hello.c
</code></pre>

<p>Adding multilib was cheaper and easier than I anticipated, and it’s thanks
to Peter0x44. It’s already been convenient for me for several projects.</p>

<p>In the past you needed w64devkit’s <code>bin/</code> on your <code>$PATH</code> in order to use
the compiler, because that’s how GCC found the other tools it needs. This
is no longer the case, and you can invoke <code>path/to/gcc.exe</code> from scripts
and such without touching your path. This required a small GCC patch.</p>

<p>The standard COFF object format supports up to 65,535 sections. When the
format was designed this probably seemed like more than enough, but modern
C++ programs can easily exceed it, particularly debug builds of programs
using lots of lambda functions. As C++ projects grow they eventually hit a
threshold where builds fail unless you ask for the <a href="https://peter0x44.github.io/posts/bigobj_format_explained/">bigobj</a> COFF format,
which supports over 4 billion sections. This is annoying, and the tools
ought to deal with this automatically.</p>

<p>A couple years ago I patched Bintutils to produce bigobj by default so the
old limit would never affect a build. The catch is that Binutils lacks an
interface to request standard COFF, so you can’t downgrade if needed. Why
would you need to? When interacting some simpler linkers like the official
Go toolchain (gc)! Some linkers can’t consume bigobj. It also breaks some
builds that auto-detect the toolchain’s object format, as some detection
scripts don’t know about bigobj (ex. libbacktrace at the time).</p>

<p>Ideally Binutils would quietly upgrade to bigobj as needed. Only large
programs see bigobj. Naive linkers and detection scripts only deal with
standard COFF. Well, thanks to Peter0x44 that’s now what Bintutils does
upstream! My bigobj hack is no longer necessary, and cgo works again.</p>

<p>This past year I’ve decided to put less emphasis on making w64devkit as
small as possible, and I’m willing to increase the installation and
distribution size for worthwhile features. This is embodied by switching
from <code>-Os</code> to <code>-O2</code> for static runtimes and tools that are computationally
expensive. Everything is now a bit bigger and a bit faster. Programs that
spend significant time in the standard library will be a little faster,
too. Programs already designed for performance will see no difference.</p>

<h3 id="new-tools">New tools</h3>

<p>CMake, Ninja, and <a href="/blog/2026/04/07/">a new, graphical CMake debugger</a> are all now
included. They require at least Windows 7. I’ve patched CMake to default
to Ninja, as CMake’s default is to look for a Visual Studio installation.
You can still <code>-G MinGW Makefiles</code> for GNU Make instead of Ninja, but I
don’t recommend it. Ninja is faster and more robust.</p>

<pre class="highlight"><code>$ cmake -B build       # configure for Ninja
$ cmake --build build  # build with Ninja
</code></pre>

<p><code>ccmake</code> is included (a Peter0x44 suggestion), a TUI front-end to examine
and modify CMake configurations. It’s been useful, more than I expected.
You can pass <code>-j</code> for parallel builds, but I strongly recommend using the
environment instead, e.g. in your <code>.profile</code>:</p>

<pre class="highlight"><code>export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
export CTEST_PARALLEL_LEVEL=$(nproc)
</code></pre>

<p>Yes, parallel testing, too! I also recommend multi-config if that makes
sense for you:</p>

<pre class="highlight"><code>$ cmake -B build -G 'Ninja Multi-Config'
$ cmake --build build --config Debug
$ cmake --build build --config Release
</code></pre>

<p>I was tempted to make this default, but it changes the build tree layout
(<code>Debug/</code> and <code>Release/</code> directories) and a shocking percentage of real
world <code>CMakeLists.txt</code> break under multi-config because they’re written
incorrectly. The world needs a good CMake linter, which could catch most
cases statically.</p>

<p>Complementing is Ccache, with special patches from Peter0x44 to improve
Windows support. If you switch between branches often then using <code>ccache</code>
will speed up your (re-)builds. I also set up the conventional Ccache
<code>lib/ccache/</code> directory. It’s a directory in w64devkit that when added
your <code>$PATH</code> transparently backs all your builds with Ccache. On a typical
Linux distribution this would be <code>/usr/lib/ccache/</code>. You can enabled it in
your <code>w64devkit.ini</code> using <code>path type</code>, too:</p>

<pre class="highlight"><code>path type = minimal+ccache
</code></pre>

<p>In my case Ccache hasn’t been useful as I hoped when I added it. I’ve
adopted Git worktrees workflow instead, so each branch sits still with its
own build tree(s).</p>

<p>To aid COM programming, the kit now includes <code>widl</code> and <code>uuidgen</code>, the
open source alternative to Microsoft MIDL, for generating Interface
Definition Language (IDL) files. <code>uuidgen</code> is a <a href="https://github.com/skeeto/w64devkit/blob/master/src/uuidgen.c">minimalist rewrite by
yours truly</a> of <a href="https://learn.microsoft.com/en-us/windows/win32/rpc/generating-interface-uuids">the Microsoft tool</a>.</p>

<p><a href="https://github.com/skeeto/w64devkit/pull/392">G. Berthiaume wrote</a> a new tool, <code>make2compdb</code>, that
<a href="https://github.com/skeeto/w64devkit/issues/251">extracts</a> JSON Compilation Database files, <code>compile_commands.json</code>,
from Make builds. It operates as a unix filter:</p>

<pre class="highlight"><code>$ make -Bwn | make2compdb &gt;compile_commands.json
</code></pre>

<p>I wrote a <code>recycle</code> tool that sends files and folders to the recycle bin,
which for the latter is usually faster than deleting them with <code>rm -rf</code>.
The <code>trash</code> tool on other systems. I rarely delete files anymore, instead
using this command to send them off to sit in the recycle bin for a couple
weeks until Storage Sense automatically deletes them.</p>

<p>A <a href="https://nullprogram.com/blog/2026/03/29/#quiltcpp">few months ago</a> I announced the addition of <code>quilt</code>, an old
school patch management tool. The original doesn’t support Windows, so
this is a <a href="https://github.com/skeeto/quilt.cpp">full rewrite in C++</a>. Now that every platform has
Quilt, w64devkit’s patches are now managed with Quilt.</p>

<p>NSIS, an installer creator, is now included, so you can build your own
application installers. Just the command line tool, <code>makensis</code>. It’s also
in a “multilib” configuration, and x64 w64devkit can produce 32-bit and
64-bit installers. (My motivation to add NSIS was my <a href="https://github.com/skeeto/dumb-alt-tab">alternative alt-tab
switcher</a>, which requires an installer to install properly.)</p>

<p>Zstandard <code>zstd</code>/<code>unzstd</code> is now included because source tarballs are
often in <code>.tar.zst</code> format. It’s a great compression format, and ought to
be your default (rather than gzip) when you need compression. busybox-w32
<code>tar</code> knows to use <code>zstd</code> for <code>.tar.zst</code>.</p>

<h3 id="runtimes">Runtimes</h3>

<p>Unique to w64devkit, C11 threads are now part of the Mingw-w64 runtime. A
new implementation by yours truly, independent of winpthreads, smaller
than winpthreads, and requires no special compiler or linker flags. (If
you’re careful, you can even use it in <a href="/blog/2023/02/15/">CRT-free programs</a>.) The
catch is that it requires Windows 7 or later because I built it on <a href="/blog/2024/10/03/">SRW
locks</a>; XP would have required substantial complexity.</p>

<p>Writing an implementation taught me that <em>C11 threads are underspecified
poorly designed</em> — in case it wasn’t already obvious by the presence of
<a href="https://blog.stephencleary.com/2013/04/recursive-re-entrant-locks.html">recursive locks</a>! This C standard addition did not get nearly the
attention is needed and should have been cut (<a href="/blog/2023/02/11/">par for the course</a>).
My implementation excludes recursive locks. Trying to create one always
fails. It also has a weakened <code>thrd_current</code> because the semantics don’t
map onto the Windows threading model.</p>

<p><code>std::terminate</code> actually terminates (traps) instead of calling <code>exit</code>, so
it traps in GDB for inspection. This includes uncaught exceptions. It also
no longer prints a half-baked stack trace, shedding ~100k of dead weight
from most C++ programs. I did this in two steps a year apart, the second
just recently. I misplaced the trap in the first change, preventing it
from shedding as much weight as it could.</p>

<h3 id="future-directions">Future directions</h3>

<p>Years ago I disabled Link-Time Optimization (LTO) due to bugs in GCC and
Binutils. Just having it available in the toolchain triggered LTO bugs.
These problems reduced my faith that LTO could produce correct programs,
so I disabled it.</p>

<p>However, I’m toying with the idea of not only re-enabling LTO, but even
distributing “FatLTO” runtimes, particularly for C++ and Fortran. That is,
runtime object files will contain both native code (at <code>-O2</code> per above)
and LTO bytecode. If you don’t request LTO, you get the pre-compiled
native code just like today, at no link-time cost. If you enable LTO, it
essentially rebuilds the runtime itself at link time. Very computationally
expensive, but you get full control, and you can even coerce it back to
the old <code>-Os</code> (or even <code>-Oz</code>) if you prefer that.</p>

<p>As far as I can tell, nobody actually distributes a toolchain with FatLTO
runtimes, so w64devkit might be the first. I know I’m the first to cross
compile FatLTO objects with GCC — remember that everything in w64devkit is
cross-compiled — because it’s never worked correctly in a GCC release. My
first attempt produced a toolchain that didn’t work.</p>

<p>I’m feeling more confident this time around because fixing LTO bugs is now
very easy: Strap a frontier AI into a good coding harness, show it what’s
broken, give it all the sources (<code>source.tar</code>), and ~15 minutes later I
have a patch. We live in a sci-fi world. I started by fixing known-to-me
LTO bugs, enabled FatLTO, then worked through newly-discovered LTO bugs.
I’ve been dogfooding it, and nothing new has popped up in awhile, but it
will take some time for me to feel confident. I’m less confident in my
ability to discover Fortran runtime bugs. Upstream GCC <a href="https://gcc.gnu.org/ai-policy.html">rejects these
fixes out of hand</a>, so through no choice of my own w64devkit will have
a unique edge in this space.</p>

<p>While some of the new features require at least Windows 7, I’m still
committed to supporting Windows XP as a baseline for the x86 release. I
keep an old XP laptop on hand, on which I test and enjoy w64devkit. You
just have to make do with some older tools, but that’s what you were
already doing anyway.</p>

]]>
    </content>
  </entry>
  <entry>
    <title>A custom virtual machine for the Stars! 4X game</title>
    <link rel="alternate" type="text/html" href="https://nullprogram.com/blog/2026/09/17/"/>
    <id>urn:uuid:0b80f9d8-07a1-4d9f-a26d-f119b08364da</id>
    <updated>2026-09-17T02:00:00Z</updated>
    <category term="c"/><category term="game"/>
    <content type="html">
      <![CDATA[<p><a href="https://en.wikipedia.org/wiki/Stars!">Stars!</a> is a 1995 <a href="https://en.wikipedia.org/wiki/4X">4X</a> game (explore, expand, exploit, exterminate)
for 16-bit Windows 3.1 that I first played ~28 years ago. While Windows is
famously backwards compatible, it’s notoriously difficult to play Stars!
today. Windows x64 cannot run 16-bit applications, and playing requires
either retro hardware or emulation (<a href="https://github.com/otya128/winevdm">otvdm</a>, <a href="https://www.dosbox.com/">DOSBox</a>), sometimes
paired with <a href="https://www.winehq.org/">Wine</a>. My new, exciting solution, <strong><a href="https://github.com/skeeto/StarsVM">Stars!VM</a></strong>, or
<em>Stars! Virtual Machine</em>, embeds a custom <a href="/blog/2014/12/09/">80286</a> emulator and a Win16
to Win32 bridge. As native Win32, the game looks and feels exactly as it
did originally, except sporting a modern file chooser and 4k scaling. It’s
indistinguishable from a genuine 32-bit or 64-bit port of the game,
especially with the original 16-bit game embedded inside the VM
executable.</p>

<p><img src="/img/stars.png" alt="" /></p>

<p>The <a href="/blog/2026/04/25/">signed</a> releases on GitHub embed a compressed copy of the original
16-bit game, so that single EXE is ready to play out-of-the-box with no
further setup or downloads. I’m distributing 32-bit builds (but requires
SSE2) because there’s no advantage to 64-bit here, and these builds work
(almost) everywhere <em>except</em> 16-bit Windows. 32-bit Windows could run the
original 16-bit game, but the VM-encapsulated version is better behaved.
It doesn’t dump a <code>Stars.ini</code> under <code>C:\WINDOWS</code>, it interacts properly
with the task bar, and copy protection is neutralized via the OS bridge.</p>

<p>If you ever been curious about Stars!, now’s the time to try it. The game
has a thorough, built-in tutorial, but also check out the <a href="https://archive.org/download/manual_Stars/Stars.pdf">manual</a>,
<a href="https://wiki.starsautohost.org/">wiki</a>, <a href="https://starsautohost.org/strategy/guidef/SSG.htm">strategy guide</a>, and <a href="https://starsautohost.org/stars.htm">AutoHost</a> (play-by-email service).
The game predates the modern search engine concept, otherwise they might
have chosen a better name. I suggest using “stars 4x” in your searches.</p>

<p>If you want to build from source and hack on the VM yourself, the best
tool for the job is <a href="https://github.com/skeeto/w64devkit">w64devkit</a>, of course, because it <a href="/blog/2020/09/25/">comes with
everything you’ll need</a>. Plus the game itself: <code>stars.exe</code>
from <a href="https://wiki.starsautohost.org/wiki/Downloads"><code>stars27jrc3.zip</code></a>.</p>

<h3 id="implementation-details">Implementation details</h3>

<p>The emulator itself requires x86 or x86-64 because it does not implement
x87 (80-bit floating point) in software, but instead runs these operation
directly on the host’s x87 hardware. This is simple, fast, and precise.
The project validates the emulation as a whole with a differential fuzzer
against the host. The fuzzer randomly generates a 16-bit instruction,
emulates it, then runs it with <a href="/blog/2015/03/19/">JIT</a> on the host and compares the
results.</p>

<p>Handles on Windows are pointer-sized, and so the Win16-to-Win32 bridge
maps 16-bit handles to host handles. It marshals between different struct
layouts when translating these calls, services the DOS interrupts the game
requires, an copies data in and out of guest memory. It’s rather like
running a <a href="/blog/2026/01/01/">Wasm</a> instance, which of course makes sense in retrospect.</p>

<p>The Win32 bridge is also monitorable and manipulatable using the Model
Context Protocol (MCP). AI agents can “see” the UI “DOM” as it’s built,
and can drive it by injecting synthetic events into the event pump, all
without going through the usual desktop control. The MCP can also read and
write guest memory. Opus 5 played a complete game through MCP — which is
quite fun to watch — requesting my assistance at just two points when it
got stuck in the UI. A foundation for a new Stars!Bench?</p>

<p>Targeting old 16-bit computers, the authors couldn’t afford to build a
sloppy, wasteful UI, and so by modern standards the game UI is remarkably
fast and responsive. They don’t make ‘em like they used to. Computing the
next turn, or “turn generation,” is the computational bottleneck, and so
that’s where I focused my optimization efforts. The emulator can trace
executed instructions, so I gathered traces of turn generation, then had
Fable 5.1 identify and reverse engineer the hottest common routines (e.g.
the game’s L’Ecuyer MCG PRNG) and basic blocks. Each was re-written in C
and mapped into the instruction decoder as new 80286 instructions. On load
the emulator identifies these routines and patches them with the new
instruction. This resulted in a nearly ~2x speedup of turn generation. By
exploiting local conditions, emulating a particular known program, I get
JIT performance without JIT complexity.</p>

<p>The original game doesn’t use <a href="/blog/2023/02/13/">buffered I/O</a>, and instead issues many
small reads and writes. Passing these small reads/writes straight to Win32
made I/O take ~5% turn generation time, probably worse today than it was
back then. Plus it’s just rude. The emulator buffers the game’s I/O calls,
further speeding up turn generation.</p>

<p>The game has some sound effects in the “battle VCR” and the final version
of the game shipped with Microsoft’s <code>WaveMix.dll</code>. Rather than load and
link this DLL, the emulator implements the DLL’s interfaces natively, and
these routines are dynamically linked into the 16-bit process. You will
not need this DLL with the emulator, nor is it embedded in releases.</p>

<p>The game also has art assets embedded uncompressed in the original EXE,
forming the bulk of its ~3MB. Stars!VM uses a custom LZ-based compression
algorithm tailored to compressing the original game. It’s compressed when
embedded in releases. So the 32-bit version of the game is half the size
of the original, at ~1.5MB.</p>

<p>The original game requires a serial code, serving as its copy protection.
A code is 8 alpha-numeric characters that must pass two checks. Failing
the first is loud, but failing the second will sabotage your game with
penalties. You’ll know because it will announce that your people suspect
you are a usurper. Most codes you’ll find online are such “usurper” codes.
Play-by-email (PBEM) saves embed a hardware signature derived from C and D
drive configuration. People playing on different machines using the same
serial code recieve the usurper penalty.</p>

<p>I bought a serial code back in the day, but it hasn’t been possible to
purchase one a for at least decade now. So the emulator injects a fixed
serial code on first run (disable with <code>--prompt-serial</code>), and you won’t
need to worry about it. The VM also produces a fixed hardware signature
(same as any other emulator), so it looks like everyone running Stars!VM
is sharing the a machine, meaning no penalty for key reuse. I cracked the
serial code checks anyway, allowing me discover interesting ones. My
favorites: CLONEMUM, CROSSNUT, EGGSWAIN, GHOSTKIN, GONKSHOW, GRIPMIME,
SEEKCAPS, SIFTBOLD, SIRBUOYS, SLIMFAZE, SLOTMOPS, SPAWNELK, SUNBLOND,
WANTNEAR, and WRONGPOX. These look like some of <a href="/blog/2017/07/27/">my passwords</a>.</p>

<h3 id="endless-possibilities">Endless possibilities</h3>

<p>I’m quite pleased and excited with the results, especially for a weekend
project. It’s breathed life back into the game for me, not only having a
better experience running it, but also that I can trivially bend the game
to my will in the ways I dreamed about. A few hooks in the right places
should open the game to easy modding, but I’m more engineer than modder.</p>

]]>
    </content>
  </entry>
  <entry>
    <title>Concurrent, atomic MSI hash tables</title>
    <link rel="alternate" type="text/html" href="https://nullprogram.com/blog/2026/05/06/"/>
    <id>urn:uuid:d877f4c2-b213-4af7-8fb9-269558ee6b86</id>
    <updated>2026-05-06T02:01:17Z</updated>
    <category term="c"/>
    <content type="html">
      <![CDATA[<p>Readers will be familiar with <a href="/blog/2022/08/08/">Mask-Step-Index (MSI) hash tables</a>, a
technique for building fast, open-addressed hash tables in <a href="/blog/2025/01/19/#flat-hash-map">a dozen lines
of code</a>. If multiple threads or processes access an MSI table with
at least one still inserting elements, care must be taken to avoid data
races. This article will show how to add atomic operations to MSI tables
in order to support different concurrency constraints.</p>

<p>Let’s begin with the simplest case: An integer hash set, no deletions,
only one insert thread (single producer), and consumers do not care about
insert order. That is, the producer inserts A then B, but consumers may
observe B in the table before A. Suppose this is the hash table in the
single-threaded case:</p>

<pre class="highlight"><code><span class="t">int32_t</span> *<span class="f">lookup</span>(<span class="t">int32_t</span> key, <span class="t">int32_t</span> *table, <span class="t">int</span> exp)
{
    <span class="t">uint64_t</span> hash = ((<span class="t">uint64_t</span>)key * <span class="m">1111111111111111111u</span>) &gt;&gt; <span class="m">32</span>;
    <span class="t">uint32_t</span> mask = ((<span class="t">uint32_t</span>)<span class="m">1</span> &lt;&lt; exp) - <span class="m">1</span>;
    <span class="t">uint32_t</span> step = (hash &gt;&gt; (<span class="m">32</span> - exp)) | <span class="m">1</span>;
    <span class="k">for</span> (<span class="t">uint32_t</span> index = hash;;) {
        index = (index + step) &amp; mask;
        <span class="k">if</span> (!table[index] || table[index]==key) {
            <span class="k">return</span> table + index;
        }
    }
}
</code></pre>

<p>Keys must be non-zero, and tables are zero-initialized. Usage example:</p>

<pre class="highlight"><code>    <span class="c">// Initialization</span>
    <span class="k">enum</span> { exp = <span class="m">8</span> };
    <span class="t">int32_t</span> table[<span class="m">1</span>&lt;&lt;<span class="m">8</span>] = {};

    <span class="c">// Producer</span>
    <span class="k">for</span> (<span class="t">int</span> i = <span class="m">0</span>; i &lt; nkeys; i++) {
        *lookup(keys[i], table, exp) = keys[i];
    }

    <span class="c">// Consumer</span>
    <span class="t">int32_t</span> key = <span class="m">1234</span>;
    <span class="t">bool</span> present = *lookup(key, table, exp);
</code></pre>

<p>The only problem is the data race on <code>table</code> slots. Since consumers can
tolerate out-of-order insertions, ordering does not matter and relaxed
atomics eliminate the data race. Insert and query now have different
requirements, so it makes sense to distinguish them. Starting with the
latter:</p>

<pre class="highlight"><code><span class="t">bool</span> <span class="f">contains</span>(<span class="t">int32_t</span> key, <span class="t">int32_t</span> *table, <span class="t">int</span> exp)
{
    <span class="t">uint64_t</span> hash = ((<span class="t">uint64_t</span>)key * <span class="m">1111111111111111111u</span>) &gt;&gt; <span class="m">32</span>;
    <span class="t">uint32_t</span> mask = ((<span class="t">uint32_t</span>)<span class="m">1</span> &lt;&lt; exp) - <span class="m">1</span>;
    <span class="t">uint32_t</span> step = (hash &gt;&gt; (<span class="m">32</span> - exp)) | <span class="m">1</span>;
    <span class="k">for</span> (<span class="t">uint32_t</span> index = hash;;) {
        index = (index + step) &amp; mask;
        <span class="t">int32_t</span> k = __atomic_load_n(table+index, __ATOMIC_RELAXED);
        <span class="k">if</span> (!k) {
            <span class="k">return</span> <span class="k">false</span>;
        } <span class="k">else</span> <span class="k">if</span> (k == key) {
            <span class="k">return</span> <span class="k">true</span>;
        }
    }
}
</code></pre>

<p>Note how all elements are accessed by atomic loads, as a producer may
store to any slot at any time. Now producers:</p>

<pre class="highlight"><code><span class="t">bool</span> <span class="f">insert</span>(<span class="t">int32_t</span> key, <span class="t">int32_t</span> *table, <span class="t">int</span> exp)
{
    <span class="t">uint64_t</span> hash = ((<span class="t">uint64_t</span>)key * <span class="m">1111111111111111111u</span>) &gt;&gt; <span class="m">32</span>;
    <span class="t">uint32_t</span> mask = ((<span class="t">uint32_t</span>)<span class="m">1</span> &lt;&lt; exp) - <span class="m">1</span>;
    <span class="t">uint32_t</span> step = (hash &gt;&gt; (<span class="m">32</span> - exp)) | <span class="m">1</span>;
    <span class="k">for</span> (<span class="t">uint32_t</span> index = hash;;) {
        index = (index + step) &amp; mask;
        <span class="k">if</span> (!table[index]) {
            __atomic_store_n(table+index, key, __ATOMIC_RELAXED);
            <span class="k">return</span> <span class="k">true</span>;
        } <span class="k">else</span> <span class="k">if</span> (table[index] == key) {
            <span class="k">return</span> <span class="k">false</span>;
        }
    }
}
</code></pre>

<p>This function may load elements non-atomically because there’s only one
producer: the current thread. This idea could not be expressed were the
type system involved, e.g. <code>_Atomic</code>, but <a href="https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html">GCC atomics</a> do not
involve require such special qualifiers. Stores on the other hand are
concurrent with consumers, requiring an atomic store. Single-producer,
multiple-consumer (SPMC) usage is nearly identical to the single-threaded
case:</p>

<pre class="highlight"><code>    <span class="c">// Producer</span>
    <span class="k">for</span> (<span class="t">int</span> i = <span class="m">0</span>; i &lt; nkeys; i++) {
        insert(keys[i], table, exp);
    }

    <span class="c">// Consumer</span>
    <span class="t">int32_t</span> key = <span class="m">1234</span>;
    <span class="t">bool</span> present = contains(key, table, exp);
</code></pre>

<p>A concurrent integer hash table is contrived and unrealistic. In a real
program a key likely carries some broader semantic meaning. For example,
if that “integer” is actually a memory offset known as a pointer, then it
<em>points</em> at some object, and it is important that stores to that object
happen before consumers observe the pointer in the table:</p>

<pre class="highlight"><code><span class="t">bool</span>   <span class="f">insert</span>(<span class="t">Thing</span> *thing, <span class="t">Thing</span> **table, <span class="t">int</span> exp)
<span class="t">Thing</span> *<span class="f">lookup</span>(<span class="t">Key</span> key, <span class="t">Thing</span> **table, <span class="t">int</span> exp)
</code></pre>

<p>Where usage might look like:</p>

<pre class="highlight"><code>    <span class="c">// Producer</span>
    <span class="k">for</span> (<span class="t">int</span> i = <span class="m">0</span>; i &lt; nthings; i++) {
        things[i].key = ...;  <span class="c">// update/init object</span>
        insert(things+i, table, exp);
    }

    <span class="c">// Consumer</span>
    <span class="t">bool</span> present = !!find((<span class="t">Key</span>){...}, table, exp);
</code></pre>

<p>In this case relaxed atomics are insufficient. Updates to the inserted
object may be reordered after the insertion, and consumers will race on
those updates. In this case we upgrade to acquire-release:</p>

<pre class="highlight"><code><span class="t">Thing</span> *<span class="f">lookup</span>(<span class="t">Key</span> key, <span class="t">Thing</span> **table, <span class="t">int</span> exp)
{
    <span class="c">// ...</span>
    <span class="k">for</span> (...) {
        <span class="c">// ...</span>
        <span class="t">Thing</span> *thing = __atomic_load_n(table+index, __ATOMIC_ACQUIRE);
        <span class="k">if</span> (!thing || thing-&gt;key==key) {
            <span class="k">return</span> thing;
        }
    }
}

<span class="t">bool</span> <span class="f">insert</span>(<span class="t">Thing</span> *thing, <span class="t">Thing</span> **table, <span class="t">int</span> exp)
{
    <span class="c">// ...</span>
    <span class="k">for</span> (...) {
        <span class="c">// ...</span>
        <span class="k">if</span> (!table[index]) {
            __atomic_store_n(table+index, thing, __ATOMIC_RELEASE);
            <span class="k">return</span> <span class="k">true</span>;
        } <span class="k">else</span> <span class="k">if</span> (table[index]-&gt;key == thing-&gt;key) {
            <span class="k">return</span> <span class="k">false</span>;
        }
    }
}
</code></pre>

<p>In this case producer and consumer synchronize on the atomics. Producer
stores are ordered before the release, and consumer loads are ordered
after the acquire. Objects are not modified once in the table, so atomics
are not required for their fields. On some architectures, including x86,
there will be no indication at the ISA level that atomics are in use —
i.e. this likely generates the same code as the single-threaded version —
and these atomics merely constrain the compiler’s instruction scheduling.</p>

<p>As a side effect of synchronizing, consumers will now observe insertions
in the same order as the producer. This is a more realistic and practical
situation than an integer hash table.</p>

<h3 id="multiple-producers">Multiple producers</h3>

<p>The multiple-producer case (MPMC) is more complicated for producers, but
consumers are unaffected, so we need only modify insertion. Still without
any locks, we will optimistically update the table. We look at the current
slot item, and if nothing is present compare-and-swap the new element in
place. On failure we <em>acquire</em> the element that won the race, continuing
as though it’s what we saw in the first place.</p>

<pre class="highlight"><code><span class="t">bool</span> <span class="f">insert</span>(<span class="t">Thing</span> *thing, <span class="t">Thing</span> **table, <span class="t">int</span> exp)
{
    <span class="c">// ...</span>
    <span class="k">for</span> (...) {
        <span class="c">// ...</span>
        <span class="t">Thing</span> *current = __atomic_load_n(table+index, __ATOMIC_ACQUIRE);
        <span class="k">if</span> (!current) {
            <span class="t">int</span> pass = __ATOMIC_RELEASE;
            <span class="t">int</span> fail = __ATOMIC_ACQUIRE;
            <span class="k">if</span> (__atomic_compare_exchange_n(
                    table+index, &amp;current, thing, <span class="m">0</span>, pass, fail)) {
                <span class="k">return</span> <span class="k">true</span>;
            }
        }
        <span class="k">if</span> (current-&gt;key == thing-&gt;key) {
            <span class="k">return</span> <span class="k">false</span>;
        }
    }
}
</code></pre>

<p>This is quite similar <a href="/blog/2023/09/30/#as-a-concurrent-hash-map">my hash trie concurrency enhancement</a> a few
years ago.</p>

]]>
    </content>
  </entry>
  <entry>
    <title>I have officially retired from Emacs</title>
    <link rel="alternate" type="text/html" href="https://nullprogram.com/blog/2026/04/26/"/>
    <id>urn:uuid:91357133-9d2d-4a6f-9b39-4bd1d35c814e</id>
    <updated>2026-04-26T00:00:00Z</updated>
    <category term="ai"/><category term="cpp"/><category term="emacs"/><category term="elfeed"/>
    <content type="html">
      <![CDATA[<p>This article was discussed <a href="https://web.archive.org/web/20260627131659/https://old.reddit.com/r/emacs/comments/1svziwa/skeeto_i_have_officially_retired_from_emacs/">on reddit</a> and <a href="https://news.ycombinator.com/item?id=47906651">on Hacker News</a>.</p>

<p>This past Tuesday I typed <code>C-x C-c</code> in Emacs for the last time after 20
years of daily use. Though <a href="/blog/2017/04/01/">nearly half that time</a> was gradually
retiring it, switching to modal editing, then to Vim. Emacs is a platform,
and I’d grown accustomed to its applications, especially those I built
myself. There was no particular hurry, so replacements came slowly. With
my <a href="/blog/2026/03/29/">newly-acquired superpowers</a> I could knock out the last two pieces
in a few days’ work, namely <a href="/blog/2009/06/23/"><code>M-x calc</code></a> with <strong><a href="https://github.com/skeeto/stackcalc">stackcalc</a></strong> and
<a href="/blog/2013/09/04/">Elfeed</a> with <strong><a href="https://github.com/skeeto/Elfeed2">Elfeed2</a></strong>. I’m especially excited about the
latter because it already exceeds the original. Both are multi-platform,
native C++ GUI applications using native UI components.</p>

<!--more-->

<p><img src="/img/elfeed2.png" alt="" /></p>

<p>These <a href="https://melpa.org/#/?q=skeeto">actively-in-use</a> packages require new maintainers (apply on
the project’s issues/discussion):</p>

<ul>
  <li><a href="https://github.com/skeeto/at-el/">@</a> (<a href="/blog/2013/04/07/">about</a>)</li>
  <li><del><a href="https://github.com/skeeto/emacs-aio/">aio</a> (<a href="/blog/2019/03/10/">about</a>)</del></li>
  <li><a href="https://github.com/skeeto/bitpack/">bitpack</a></li>
  <li><del><a href="https://github.com/skeeto/elfeed/">Elfeed</a> (<a href="https://github.com/skeeto/elfeed/discussions/563">apply here</a>)</del></li>
  <li><del><a href="https://github.com/skeeto/impatient-mode/">Impatient</a> (<a href="/blog/2012/08/20/">about</a>)</del></li>
  <li><a href="https://github.com/skeeto/javadoc-lookup/">javadoc-lookup</a> (<a href="/blog/2013/01/30/">about</a>)</li>
  <li><a href="https://github.com/skeeto/elisp-json-rpc/">json-rpc</a></li>
  <li><a href="https://github.com/skeeto/emacs-memoize/">memoize</a> (<a href="/blog/2010/07/26/">about</a>)</li>
  <li><del><a href="https://github.com/skeeto/nasm-mode/">nasm-mode</a> (<a href="/blog/2015/04/19/">about</a>)</del></li>
  <li><del><a href="https://github.com/skeeto/emacs-web-server/">simple-httpd</a> (<a href="/blog/2012/08/20/">about</a>)</del></li>
  <li><a href="https://github.com/skeeto/skewer-mode/">Skewer</a> (<a href="/blog/2012/10/31/">about</a>)</li>
  <li><del><a href="https://github.com/skeeto/elisp-weak-ref/">weak-ref</a> (<a href="/blog/2012/12/17/">about</a>)</del></li>
  <li><a href="https://github.com/skeeto/x86-lookup/">x86-lookup</a> (<a href="/blog/2015/11/21/">about</a>)</li>
</ul>

<p>No wonder it took so long for me to move on! I’m not handing these off to
just anyone, and you’ll need to establish your reputation. Having already
made contributions is a good sign, even if never merged. I’m willing to
transfer them off my namespace, though you’ll need to manage the Melpa
hand-off (on which I’ll sign-off). If there are no takers, these projects
will be archived but not deleted.</p>

<h3 id="trying-out-wxwidgets">Trying out wxWidgets</h3>

<p>The Emacs Calculator is amazing and the best calculator I’ve ever used,
which is why nothing I could find was going to replace it. My clone uses
GMP and MPFR for multi-precision, so it’s far faster, as to be expected,
but it’s not nearly at feature parity. It’s missing esoteric features
including symbolic processing. Though it’s enough to cover all of my own
usage. I can add more features later. The Emacs Calculator manual served
as a specification when building stackcalc.</p>

<p>Elfeed has been a cornerstone of my daily routines for the past 13 years.
Nothing else I’ve found scratches that itch for me, so I’ve always known
it would require a rewrite someday. Knowing it would take a few weeks of
work, and that I <em>already had the feed reader I wanted</em>, made motivation
difficult to find. Though now that I can accomplish ~3 weeks of old-way
work in a new-way day, this sort of project becomes that much easier to
start and finish. Though it’s not yet at a 1.0 release, after a couple
days Elfeed2 was working well enough to replace the original Elfeed.</p>

<p>While <a href="https://github.com/ocornut/imgui">Dear ImGui</a> was the right choice for <a href="https://github.com/skeeto/dcmake">dcmake</a>, it would not be
so for these two applications. Active rendering doesn’t suit a feed reader
left running all day, and I needed a richer toolkit. Professionally I work
in Qt, but I wanted something lighter-weight for my projects, accessible
via CMake <code>FetchContent</code>. That naturally led to <a href="https://wxwidgets.org/">wxWidgets</a>. While it
has issues — mitigatable character encoding problems, accidental quadratic
time in many places — it’s worked better than I anticipated, letting me
rapidly produce native-looking applications on Windows, macOS, and Linux.</p>

<p>Unlike Dear ImGui, wxWidgets is a platform, including <a href="/blog/2021/12/30/">sane</a> I/O and
path handling. I <em>mostly</em> don’t need platform layers when building
applications like these. I can simply rely on wxWidgets’ utilities.</p>

<p>Both of these projects build out-of-the-box on <a href="https://github.com/skeeto/w64devkit">w64devkit</a> thanks to the
dependencies being <code>FetchContent</code>-compatible. On all platforms you just
need a C++ toolchain and CMake:</p>

<pre class="highlight"><code>$ cmake -B build
$ cmake --build build
</code></pre>

<p>Now that I have experience with wxWidgets, learning its limitations and
capabilities, it’s likely to be a foundation of most of my GUI projects to
come, except where something like Dear ImGui is a better git.</p>

]]>
    </content>
  </entry>

</feed>
