<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mantel on hereticles</title><link>https://icle.es/tags/mantel/</link><description>Recent content in Mantel on hereticles</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sun, 21 Jun 2026 17:14:49 +0100</lastBuildDate><atom:link href="https://icle.es/tags/mantel/index.xml" rel="self" type="application/rss+xml"/><item><title>Testing SSE in zig When EVERYTHING Blocks</title><link>https://icle.es/2026/06/21/testing-sse-in-zig-when-everything-blocks/</link><pubDate>Sun, 21 Jun 2026 17:14:49 +0100</pubDate><guid>https://icle.es/2026/06/21/testing-sse-in-zig-when-everything-blocks/</guid><description>&lt;p&gt;I rely on TDD heavily and really don&amp;rsquo;t like manual testing. So when I started
building SSE support into &lt;a href="https://icle.es/excursions/mantel.md"&gt;mantel&lt;/a&gt;, I wanted a proper
e2e test — connect a client, read the events, assert on them.&lt;/p&gt;
&lt;p&gt;Except, I ran into a problem.
&lt;a href="https://github.com/karlseguin/http.zig"&gt;http.zig&lt;/a&gt;&amp;rsquo;s test helpers don&amp;rsquo;t work
with SSE - they expect a request/response cycle that completes. And zig 0.16&amp;rsquo;s
new Io framework supports
&lt;a href="https://ziglang.org/download/0.16.0/release-notes.html#Batch"&gt;neither cancellation&lt;/a&gt;
&lt;a href="https://ziggit.dev/t/non-blocked-sockets-programming-in-0-16/14978"&gt;nor non-blocking reads&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So: everything blocks, and there&amp;rsquo;s no way to stop it.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I rely on TDD heavily and really don&rsquo;t like manual testing. So when I started
building SSE support into <a href="https://icle.es/excursions/mantel.md">mantel</a>, I wanted a proper
e2e test — connect a client, read the events, assert on them.</p>
<p>Except, I ran into a problem.
<a href="https://github.com/karlseguin/http.zig">http.zig</a>&rsquo;s test helpers don&rsquo;t work
with SSE - they expect a request/response cycle that completes. And zig 0.16&rsquo;s
new Io framework supports
<a href="https://ziglang.org/download/0.16.0/release-notes.html#Batch">neither cancellation</a>
<a href="https://ziggit.dev/t/non-blocked-sockets-programming-in-0-16/14978">nor non-blocking reads</a>.</p>
<p>So: everything blocks, and there&rsquo;s no way to stop it.</p>
<p>If I wasn&rsquo;t building this because it&rsquo;s fun, I&rsquo;d probably skip it. Actually, I&rsquo;d
just use go where testing this is easy.</p>
<p>I am not doing this for work, and this kind of &ldquo;threading the needle&rdquo; solution
finding is something I enjoy. Plus, I am writing zig because I love it.</p>
<p>I&rsquo;d actually decided to let it be and moved on, but in my sleep, a solution came
to me, and it worked.</p>
<h2 id="if-we-could-cancel">If We Could Cancel</h2>
<p>The normal testing pattern for this, for example in Go, would be something like:</p>
```
- Set up the server
- Connect the Client with a timeout
- Read from the client
```
<p>Here is a version of it from <a href="https://icle.es/endeavours/henge.md">henge</a>.</p>
```go
testServer := httptest.NewServer(router)
defer testServer.Close()

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
scanner := connectToDevice(ctx, testServer)
// we don't care about the initial event
scanner.Scan() // blocks until first event arrives
// ...
```
<p>You could do something similar if it didn&rsquo;t support timeout, but did support
non-blocking reads. You just wait until your timeout and see if the data was
there.</p>
<p>Unfortunately, on zig, <code>std.Io.Evented</code> — the non-blocking backend — is still a
work in progress.&quot;</p>
<h2 id="unblocking">(Un)Blocking</h2>
<p>The idea that came to me was simply that we could close the server stream
<em>before</em> we read as the client. In other words:</p>
```
- Start the Server in a new thread
- Connect Client, but don't read
- Close the server
- Read all bytes from client
  - Safe now because stream has been closed
```
<p><a href="https://icle.es/src/Server.zig">Server.zig</a></p>
```zig
test "e2e test" {
    const io = std.testing.io;
    const allocator = std.testing.allocator;

    var server = try init(io, allocator);
    defer server.deinit(allocator);

    var router = try server.https_srv.router(.{});
    router.get("/feed", handleFeed, .{});

    // start server async
    const thread = try server.https_srv.listenInNewThread();
    // The order of the following two lines is crucial
    defer thread.join();
    defer server.https_srv.stop();

    const conf_address = server.https_srv.config.address.ip.ip4;
    try httpz.testing.waitForPort(conf_address.port);

    const address = std.Io.net.IpAddress.parse("127.0.0.1", conf_address.port) catch unreachable;
    const stream = try address.connect(io, .{ .mode = .stream });
    defer stream.close(io);

    // send HTTP request first
    var write_buf: [256]u8 = undefined;
    var writer = stream.writer(io, &write_buf);
    const w = &writer.interface;
    w.writeAll("GET /feed HTTP/1.1\r\nHost: localhost\r\n\r\n") catch unreachable;
    w.flush() catch unreachable;

    // wait for feed to start
    while (!server.ctx.feed.active) {
        std.Io.sleep(io, std.Io.Duration.fromMilliseconds(100), std.Io.Clock.real) catch unreachable;
    }

    try server.ctx.feed.stopFeed();

    var read_buf: [1024]u8 = undefined;
    var reader = stream.reader(io, &read_buf);

    // drain the stream
    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(allocator);
    try reader.interface.appendRemaining(allocator, &body, .unlimited);

    try std.testing.expect(std.mem.indexOf(u8, body.items, "a message") != null);
}
```
<p>And in <a href="https://icle.es/src/Feed.zig">Feed.zig</a>, we need to ensure the stream is closed
on de-activating the feed:</p>
```zig
fn handle(feed: *Feed, stream: std.Io.net.Stream) void {
    var buf: [4096]u8 = undefined;
    var writer = stream.writer(feed.io, &buf);
    const w = &writer.interface;
    w.writeAll("a message\n") catch unreachable;
    w.flush() catch unreachable;

    while (feed.active) {
        std.Io.sleep(feed.io, std.Io.Duration.fromMilliseconds(100), std.Io.Clock.real) catch unreachable;
    }

    stream.close(feed.io);
}
```
<p>Feed.zig also contains details about the active flag and how it&rsquo;s managed</p>
```zig
pub fn startFeed(feed: *Feed, res: *httpz.Response) !void {
    try feed.mutex.lock(feed.io);
    defer feed.mutex.unlock(feed.io);

    if (!feed.active) {
        try res.startEventStream(feed, Feed.handle);
        feed.active = true;
    }
}

pub fn stopFeed(feed: *Feed) !void {
    try feed.mutex.lock(feed.io);
    defer feed.mutex.unlock(feed.io);

    feed.active = false;
}
```
<p>In this test, we are just verifying we get back &ldquo;a message&rdquo; which is just a
placeholder. The intent is to prove the mechanism. The next step is to write an
actual event and verify it.</p>
<h3 id="gotchas">Gotchas</h3>
<p>There are many gotchas in this solution, particularly with ordering.</p>
<p>The server has to be closed before we join the thread, otherwise, it&rsquo;ll just
block.</p>
<p>We have to read all the content from the stream in one go with
<code>appendRemaining</code>, otherwise, it&rsquo;ll block. We could read in parts if we know
there is a minimum of that much content left in the stream — but that&rsquo;s tricky
at best.</p>
<p>We should wait until the Feed is <code>active</code> before de-activating it, or you&rsquo;ll end
up with race conditions.</p>
<p>You must drain the stream fully, otherwise you&rsquo;ll get errors.</p>
<h2 id="why-not-just-mock-it">Why Not Just Mock It?</h2>
<p>I could mock it, and I will add some mocked tests later. I wanted some
confidence of the end to end journey working well without having to run it and
test with curl manually.</p>
<p>Once I replace the placeholder with actual events, I&rsquo;ll not feel compelled to
test it manually each time to ensure it still works.</p>
]]></content:encoded></item><item><title>Mantel: at a Glance</title><link>https://icle.es/2026/06/21/mantel-at-a-glance/</link><pubDate>Sun, 21 Jun 2026 15:33:51 +0100</pubDate><guid>https://icle.es/2026/06/21/mantel-at-a-glance/</guid><description>&lt;p&gt;After &lt;a href="https://icle.es/my-data.md"&gt;I started the migration from the cloud to my own infra&lt;/a&gt;, I
had more things to monitor, like the VPS which hosted my sites. I set up alerts
on grafana and that works well enough to alert me, but alerting has multiple
moving parts that could go wrong, and I didn&amp;rsquo;t want to rely on that fully.&lt;/p&gt;
&lt;p&gt;There were also a few things I kept an eye on every day that lived across
multiple websites:&lt;/p&gt;</description><content:encoded><![CDATA[<p>After <a href="https://icle.es/my-data.md">I started the migration from the cloud to my own infra</a>, I
had more things to monitor, like the VPS which hosted my sites. I set up alerts
on grafana and that works well enough to alert me, but alerting has multiple
moving parts that could go wrong, and I didn&rsquo;t want to rely on that fully.</p>
<p>There were also a few things I kept an eye on every day that lived across
multiple websites:</p>
<ul>
<li>Traffic to my site</li>
<li>Notifications across some platforms (e.g. GitHub)</li>
<li>Any form submissions on <a href="https://tally.so">tally.so</a></li>
</ul>
<p>I also wanted to eliminate some windows that constantly took up screen
real-estate. My music player, chat apps like fluxer and discord — I didn&rsquo;t need
the apps themselves, just a &ldquo;currently playing&rdquo; widget and unread counts.</p>
<p>I dedicated one monitor out of my three to a full overview of everything I
wanted to keep an eye on. It would replace the apps already living there anyway.</p>
<p>I looked around, and there were tools that could do some of these.
<a href="https://grafana.com/">Grafana</a> can do hosts, alerting, etc.
<a href="https://github.com/brndnmtthws/conky">Conky</a>,
<a href="https://github.com/elkowar/eww">eww</a> and its ilk could do the music player,
unread stats and host based stuff. They could be extended to do some of the
additional bits I needed.</p>
<p>But what I really wanted was something that went across those boundaries and
provided across the board, in some ways providing grafana like functionality in
a conky like interface.</p>
<p>The former is infrastructure-out giving you an overview of your estate, while
the latter is machine-in, as a lens into your system. Neither cared about things
like the traffic to my site or form submissions on tally - at least, not without
extra fiddling.</p>
<p>What I wanted was a display surface across everything. I considered shoehorning
the features into one of those tools, but getting mpris into grafana felt much
like trying to fit a round peg into a square hole. I might have more luck
building some extra widgets for eww or conky, but that would involve yuck and
c++ respectively. While I&rsquo;ve worked with c++, I had not worked with yuck before,
and neither felt like it would be fun for me.</p>
<p>I did not want to spend a lot of time writing code that I didn&rsquo;t particularly
enjoy to add functionality to software that didn&rsquo;t quite fit what I actually
wanted. There was also the possibility that I&rsquo;d get knee deep in it before I
found that it didn&rsquo;t quite do what I needed, or I needed to patch it, and have
to jump through hoops to get it in, or maintain a fork.</p>
<p>Thus <a href="https://icle.es/excursions/mantel.md">mantel</a> was born.</p>
<p>I started with a quick prototype that Claude built in go and it worked well
enough. I wasn&rsquo;t interested in building it enough to spend a lot of effort on
it.</p>
<p>Over a few weeks, I got Claude to add a few extra bits. The plan was to
eventually bring in some graphs from grafana by linking to them directly, which
should bring enough observability into one dashboard. Getting to know grafana a
bit better, I realised that some of the observability I wanted - like being able
to have a status widget with its dependencies in the various possible states
would involve a lot of learning grafana and I could see it going into UI
component fiddling. One look at the panel with all the customisation options was
enough to make me feel sleepy. I&rsquo;d rather write a bunch of code any day.</p>
<p>But it wasn&rsquo;t quite working for me. Claude did a good enough job for a
prototype, but as I wanted more features, the rough edges started to show. I
added a cache volume and to monitor that, I would have to run a script as root -
fine for now, but it could get ugly fast. There was a perceptible lag in
response. I&rsquo;d start timewarrior from the console and it would take a few seconds
before the dashboard updated. Same when I stopped it. Same with the CPU usage -
not a serious problem, but annoying.</p>
<p>I got claude to refactor some of the code to make it easier to extend, and it
worked - well enough. It didn&rsquo;t have tests and if I wanted to extend stuff, I&rsquo;d
really have to rely on Claude.</p>
<p>I am familiar with go, and like it well enough - I do a lot of work in it, but I
don&rsquo;t really program in go for pleasure anymore.</p>
<p>I considered getting to know the code - there wasn&rsquo;t a lot, but it did not fill
me with enthusiasm. Making the whole thing more responsive would involve as much
effort as a ground up rewrite, partly because there wasn&rsquo;t much, and what was
there was built around polling. Since it was working well enough, I tried to put
it off.</p>
<p>As the annoyances started mounting, and the desire for more things showing up on
the dashboard increased, it finally tipped over into a decision to build it
&ldquo;properly.&rdquo;</p>
<p>I seriously considered owning the codebase, and wiring in SSE, or rebuilding
parts of it to support SSE, but I struggled to muster the enthusiasm to write
another tool in go and then to maintain it.</p>
<h2 id="the-stack">The Stack</h2>
<p>I decided to use zig for the new version, mainly because I love it. It&rsquo;s 0.16
and likely a long way from a 1.0 release. The maintenance burden is immediately
higher because it gets a lot of breaking changes. There are fewer libraries, and
the ones that are there are less well tested.</p>
<p>There is a lot more risk associated with zig. However, I love working with it,
and I&rsquo;d rather spend far more time writing something in zig - in some ways it&rsquo;s
a feature.</p>
<p>Still, I checked to make sure that zig @ 0.16 has enough of the features to
allow me to build it without too many obstacles. Zig doesn&rsquo;t support SSE out of
the box, but there is a library
<a href="https://github.com/karlseguin/http.zig">http.zig</a> which supports it. Testing it
is a bit tricky because zig doesn&rsquo;t yet support non-blocking i/o or timeouts,
but that&rsquo;s another post.</p>
<p>The rest of what we need is mainly the ability to call other executables,
network calls and async, all of which are available, sometimes just enough - but
enough.</p>
<p>For the UI design, there are two main problems to address - I am not good at it,
and I don&rsquo;t enjoy working on it. To make it easier on myself, two things that
help are live-reload and Claude.</p>
<p>I considered <a href="https://github.com/david-vanderson/dvui">DVUI</a> which I&rsquo;d used
before. The main problem with it is that there isn&rsquo;t a tight build loop. I would
have to build and run each time, slowing it down. DVUI is also niche enough that
Claude is unlikely to be able to help me theme it.</p>
<p>Most UI toolkits tend to design for applications, and designing a dashboard that
is information dense and with a specific synthwave/retro aesthetic I have in
mind (to match the rest of my window manager theme) will involve a lot of
tweaking or going straight to rendering as graphics.</p>
<p>I would have built using a TUI, which would still be a retro aesthetic I would
enjoy, but poor image support discounts it even before the absence of live
reload.</p>
<p>Web on the other hand, would do live reload on code change pretty easily.
Flutter would too, but I&rsquo;d used it before and felt like a level of unnecessary
complexity.</p>
<p>For the frontend, web is the easiest interface to work with. Vanilla Javascript,
and lit were considered, but I&rsquo;d get annoyed with the html hiding inside quotes.</p>
<p>htmx would be cool, but I&rsquo;d be building it with div tags, while I liked the
neatness of component html tags. It also did not have live reload out of the
box.</p>
<p>Svelte is relatively simple and doesn&rsquo;t need html to be in quotes, and has live
reload. I have also played with it a little bit in the past, and enjoyed the
experience. It does however add a whole set of additional tooling requirements,
not to mention (p)npm.</p>
<p>I decided to give svelte a try first, primarily for the tight live reload. If it
ends up taking more time than is saved by the live-reload, I&rsquo;ll switch to htmx
as the cleaner option.</p>
<p>Once I have a decent handle on the svelte version, it might even make sense to
rewrite it in htmx to streamline the maintenance burden.</p>
<h2 id="concurrency-model">Concurrency Model</h2>
<p>The makeshift version of mantel polls, which is good enough, but also annoying,
particularly with <code>timewarrior</code> and <code>mpris</code>, as mentioned earlier. Server Side
Events would be a better candidate for quicker updates. We don&rsquo;t need
bidirectional communication, so WebSockets would be overkill.</p>
<p>At some point, we might want to send messages back to the server from the
dashboard, but they are likely to be few and far between. As such specific
endpoints would make more sense than full blown WebSockets.</p>
<p>In terms of collecting the data, the plan is to hook into interrupts wherever
possible. For example, I&rsquo;d want to use <code>inotify</code> to watch the timewarrior files.
We&rsquo;d then know exactly when the time starts or stops, and we can push an event
to update.</p>
<p>For MPRIS, we could listen to the dbus message bus for signals.</p>
<p>Some things, we&rsquo;d still need to poll, like the cpu, but we can do it more often
and send updates.</p>
<p>On a side note, I don&rsquo;t want to be running it as root for obvious reasons.
However, we&rsquo;ll still need data that is accessible only as root. In the
prototype, I used sudo restricted to running specific scripts that output the
data. With the new version, I&rsquo;ll get prometheus to pick up the data and mantel
can query that. It lets mantel run entirely as the current user. Running as the
current user is also important for things like MPRIS and timewarrior.</p>
<p>I&rsquo;ve used SSE before, with <a href="https://icle.es/endeavours/henge.md">henge</a> in go, and at a
contract in 2024 in java.</p>
<p>The idea is that we&rsquo;d have a few generic events:</p>
<ul>
<li><code>string</code>: single string. e.g. hostname, label</li>
<li><code>number</code>: single numeric value. e.g. inbox count</li>
<li><code>numbers</code>: array of numbers (CPU cores, memory breakdown)</li>
<li><code>state</code>: enum of <code>up</code> | <code>down</code> | <code>warn</code> | <code>err</code> | <code>unknown</code>. e.g. host</li>
<li><code>percentage</code>: <code>{ value:number, max: number}</code>. e.g. mpris track position, total
cpu/mem usage</li>
</ul>
<p>If a collector needs to send information that is tightly bundled together, we&rsquo;d
create event types for them, like for <code>mpris</code> and <code>timewarrior</code></p>
<ul>
<li><code>mpris</code>: including title, artist, album, progress, media link etc.</li>
<li><code>timew</code>: including timer status, tags, last timer tags, link to start it etc.</li>
</ul>
<p>Keeping it simple would also make it easier to migrate to htmx (or something
else) later.</p>
<h2 id="progress">Progress</h2>
<p>I&rsquo;ve already come across a rough edge of zig,
<a href="https://icle.es/test-blocking-sse.md">writing a test for SSE when everything blocks, but I enjoyed working around it</a>.</p>
<p>The more I think about it, the more it feels like <code>htmx</code> might be a better fit -
mainly because it&rsquo;s small and doesn&rsquo;t have a ton of dependencies. Once I have a
collector working, I&rsquo;ll try both out before I decide the way forward.</p>
]]></content:encoded></item></channel></rss>