<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
                        <title><![CDATA[nicoverbruggen.be]]></title>
                                <description>Atom feed for the latest 20 posts from nicoverbruggen.be</description>
                                <logo>https://nicoverbruggen.be/icons/apple-touch-icon.png</logo>
                                <link href="https://nicoverbruggen.be"></link>
                                <pubdate>2026-07-29 17:14:43</pubdate>
                                <lang>en</lang>
                    <entry>
        <title><![CDATA[Do Niche Things]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/do-niche-things" />
        <id>https://nicoverbruggen.be/280</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>One of the coolest things you can do today is work on stuff that’s pretty damn niche. You shouldn’t do it for the fame and attention, that’s not the point of working on niche stuff. The point is that you can fix problems and resolve annoyances that no one else is interested in dealing with.</p>
<p>Everyone seems to be busy building these big things, and that’s fine. But you should not discount what you can do when you start solving specific, small problems. Think about it: you just might have fixed a massive personal annoyance, which can be very rewarding. Best case scenario? You may have found a business opportunity.</p>
<p>My latest solo projects, many of which are not public, are just little things I made for myself that I love working on. For example: small apps that do the thing I want them to do, just for myself. I have also been working on PRs for projects I enjoy using, or mods for apps or games I find fun. All in all, super niche things. Yet super nice, too.</p>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Wed, 29 Jul 2026 21:00:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[Fixing Kobo&#039;s Typesetting Quirks on Firmware 4.x]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/kobo-typesetting-fixes" />
        <id>https://nicoverbruggen.be/278</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>In the past, I’ve owned both Kindle and Kobo devices, and I think when it comes to examining these devices at <em>stock</em>, there’s no doubt that a Kindle has a better reading experience.</p>
<p>Kindles ship with better fonts out of the box, and the font rendering stack, which decides <em>what text looks like</em>: you know, that thing you’re effectively staring at for the majority of your time with the device. Compared to Kindle, how text is displayed is somewhat deficient on Kobo if you know what you’re looking at.</p>
<p>I should probably start with a bit of a disclaimer. I understand that a lot of people will probably <em>never notice any of these issues</em> and are perfectly fine reading <em>even with the default settings</em>. But as someone who enjoys not only beautiful typography but also excellent visual design in books, it is something that matters to me.</p>
<h3>A quick recap</h3>
<p>I’ve blogged about it before: I don’t really like what Amazon is doing with their grip on the digital book ecosystem. Conversely, I like supporting Rakuten Kobo, the underdog. Their devices are much more open and it’s thanks to that openness that I was able to make <a href="https://kp.nicoverbruggen.be/">KoboPatch Web UI</a> so easy to use if you want to mod your device.<sup id="fnref:jailbreak"><a class="footnote-ref" href="#fn:jailbreak" role="doc-noteref">1</a></sup></p>
<p>I’ll be honest: I’m a bit of a typography nerd. A snob, some might say. It’s been that way even before I purchased my first MacBook and I was astonished by the beautiful typefaces that I’d never seen before when I was on Windows and Linux: Baskerville, Cochin, Futura, Gill Sans, Helvetica Neue, Hoefler Text…<sup id="fnref:windows_ripoff_fonts"><a class="footnote-ref" href="#fn:windows_ripoff_fonts" role="doc-noteref">2</a></sup></p>
<p>Being fascinated by typography may very well be the only reason I can think of that properly explains why I went through the process of doing <a href="https://nicoverbruggen.be/blog/patching-fonts-for-kobo">literal patching of fonts</a> just to get them to look right on my e-reader <a href="https://nicoverbruggen.be/blog/kobo-font-fix">to the point of automating the process</a>.</p>
<p>But I didn’t stop there, I also worked on a derivative<sup id="fnref:derivative"><a class="footnote-ref" href="#fn:derivative" role="doc-noteref">3</a></sup> font, <a href="https://nicoverbruggen.be/blog/creating-readerly">Readerly</a>, which was based on Newsreader with automated transformations to get to the output font.</p>
<p>I also eventually started doing manual outline editing in FontForge to create <a href="https://nicoverbruggen.be/blog/libron">Libron</a>, which is the font that I recommend most these days for anyone who would like an alternative to <a href="https://en.wikipedia.org/wiki/Bookerly">Bookerly</a>, since that is a proprietary font, and Libron is not.</p>
<p>(I should note: Libron is <em>not</em> a Bookerly clone. It intentionally isn’t, and that you can trace its origins back to Newsreader is a <em>feature</em>.)</p>
<p>Anyway, that’s a bit of a quick recap of what has happened prior to all of this. Until not too long ago, I was pretty sure my work was done. But there was one thing annoying me: something I had noticed myself, too. The wobble. The dang wobble.</p>
<h3>The curious case of the inconsistent baseline</h3>
<p>With <em>wobble</em>, I am, of course, referring to the sometimes <a href="https://github.com/nicoverbruggen/readerly/issues/1">inconsistent baseline rendering</a> of certain fonts on Kobo devices. The issue that I’ve linked illustrates it clearly, despite it not being an issue with the actual font. (Initially, I thought it had been <em>my</em> fault!)</p>
<p>Ever since, I’ve started affectionately calling this “the wobble problem”, because it sort of feels like the letters are “wobbling” on the page due to the inconsistent baseline.</p>
<p>I had dismissed the problem for some time now, because it’s less pronounced when you use the font at a large text size. But I know that people are using the fonts from the <a href="https://github.com/nicoverbruggen/ebook-fonts">eBook Fonts</a> collection at various font sizes, so I decided that it was time to properly investigate.</p>
<p>Over the last months, I’ve made numerous attempts at trying to understand what causes this particular issue, and I’ve been attempting to fix it ever since. I noticed that applying hints to a given font allows it to be rendered slightly differently, usually with good results.</p>
<p>I say usually, because sometimes the result ends up being slightly worse, which is not really what I was aiming for. So for the last little while I’ve been tinkering around with all sorts of changes in an attempt to get the Kobo renderer to correctly render the glyphs without any baseline issues.</p>
<p>I don’t think it’s acceptable that a device that’s intended to be used for reading does a poor job at rendering text.</p>
<h3>Breakthroughs via LLM</h3>
<p>I had a breakthrough recently, as I was going through the process of debugging the entire renderer with Claude Code and OpenAI’s Codex. I was informed by someone quite knowledgeable that LLMs are actually generally speaking more useful for debugging compared to actual programming.</p>
<p>An important lesson here is this: if you build tools that LLMs can use to validate output, suddenly they become a lot more interesting and reliable. Over the last months, I was using both to try to learn more about how Kobo’s rendering stack works.</p>
<p>I learned a bunch of things. Here’s some of the essentials:</p>
<ul>
<li>Kobo devices run Linux. They’re so open you can even get SSH access and remote connect to them. I was able to dump the entire filesystem for analysis because of this.</li>
<li>The main software layer is called Nickel. That’s why the popular mod is called <a href="https://pgaskin.net/NickelMenu/">NickelMenu</a>.</li>
<li>ePub files, and also Kobo-specific ePub files, are nothing but a zip archive with some HTML and CSS. They’re like mini-webpages. To display them, you need a rendering engine usually based on browser technology.</li>
<li>On Kobo devices, there are two such renderers: one is based on WebKit (used for <code>kepub</code> files) and one is Adobe’s proprietary <a href="https://www.adobe.com/be_en/solutions/ebook/rmsdk.html">RMSDK</a> which is actually migrating away from Adobe at the time of writing.</li>
<li>If you are reading store-bought books or converted <code>kepub</code> files, you can have some of the rendering issues I was running into.</li>
</ul>
<p>Relevant for our investigation are these points:</p>
<ul>
<li>The WebKit-based renderer uses an old version of WebKit as the main layout engine, but the actual text rendering happens via FreeType, which has a proprietary extension included (iType) which allows customizations of various parameters to modify how font outlines are rendered.</li>
<li>iType is likely a Monotype invention<sup id="fnref:monotype"><a class="footnote-ref" href="#fn:monotype" role="doc-noteref">4</a></sup>. The “font weight” slider in the Advanced font panel in Kobo’s reader app uses these so-called CSM (contrast/sharpness modulation?) parameters. iType is actually the driver and rasterizer.</li>
</ul>
<p>Here’s a visualization of how these libraries are loaded:</p>
<pre><code>Nickel (Qt 5.2.1 application, libnickel.so.1.0.0)
  └─ kepub books rendered via QtWebKit (Kobo-patched)
       └─ QPainter / QRawFont / drawGlyphRun
            └─ libkobo.so   (Qt platform plugin)
                 └─ custom QFontEngineFT
                      └─ libfreetype.so.6.6.2   ← FreeType API surface
                           └─ Monotype iType    ← the actual scaler/rasterizer
</code></pre>
<p>To make it easier to understand:</p>
<pre><code>   An EPUB book is like a tiny website (HTML + CSS + fonts)
        │
   ┌────▼─────────────────────────────────────────────┐
   │  Nickel        the main Kobo app (libnickel.so)  │
   │                decides fonts, injects CSS        │
   ├──────────────────────────────────────────────────┤
   │  Qt WebKit     a web browser engine (Qt 5.2.1)   │
   │                lays out the page, like Chrome    │
   ├──────────────────────────────────────────────────┤
   │  Qt font engine (QFontEngineFT, HarfBuzz)        │
   │                 from text to positioned &quot;glyphs&quot; │
   ├──────────────────────────────────────────────────┤
   │  FreeType + iType   (libfreetype.so)             │  
   │                     turns each glyph into pixels │
   └──────────────────────────────────────────────────┘
        │
   Pixels on the e-ink screen
</code></pre>
<p>I needed to find out what was causing the actual rendering issue:</p>
<ul>
<li>Was the font faulty? (Likely not.)</li>
<li>Was the text layout broken? (Perhaps?)</li>
<li>Was the outline rendering broken? (Likely?)</li>
</ul>
<p>I was willing to explore all three, but my investigation eventually led me to prompt the LLMs to create a “pixel inspector”. I also went on a whole detour where I let Claude swap out the iType renderer, so it would use stock FreeType instead (which is what is used to render the UI).<sup id="fnref:swap"><a class="footnote-ref" href="#fn:swap" role="doc-noteref">5</a></sup></p>
<p>I can validate with my own eyes if something looks right, but Claude can’t (reliably). That’s why I had it build the “pixel perfect rendering” system: Claude can validate correctness by simply checking the raw pixels, which are output to a plain text file.</p>
<p>Once the pixels aren’t the same between the before and after, there’s altered rendering. If the altered pixels are <em>only</em> where the known mistakes are, we know there’s a potential fix. I knew that there were certain sentences I could try, and I could point out where I’d expect to see the fix apply.</p>
<p>I also asked Claude to write me a report at the end, and this is what the generated report included:</p>
<p class="attributed-ai" data-provider="Claude Opus 4.8 (1M)">The breakthrough tool was a pixel-level ink inspector. After iType rasterizes a glyph into an alpha bitmap (<code>FT_Outline_Get_Bitmap</code>), the inspector scans the buffer row by row for the first and last rows containing ink (non-transparent pixels). This yields the glyph’s true rendered height in pixels, independent of metrics or claimed positions.</p>
<h3>Emulating Kobo’s binaries</h3>
<p>To do this, I needed to run some of the compiled code as-is in order to be able to reproduce the bug visually. That would allow me to confirm a fix. First, we’d render a flawed version, then we’d patch the binary, and then we’d render a fixed version.</p>
<p>So, I told Claude to get to work and surprisingly, it delivered. Using <a href="https://www.qemu.org/">QEMU</a> it was possible to run a selection of instructions as-is in order to construct the pixel inspector. It was explained to me as such:</p>
<p class="attributed-ai" data-provider="Claude Opus 4.8 (1M)">The device library is a self-contained 32-bit ARM (hardfloat) shared object that depends only on <code>libc</code> and <code>libgcc_s</code>. So it runs unmodified under <code>qemu-arm</code>. With the toolchain <code>gcc-arm-linux-gnueabihf</code> we can cross-compile a tiny harness that links the device’s FreeType library (not the distro’s). <code>qemu</code> then emulates the ARM code.</p>
<p>We’ll get back to the importance of being able to run the actual code that also runs on the device without a back and forth roundtrip, because it has implications for later issues I encountered.</p>
<h3>Where it goes wrong</h3>
<p>The problem, essentially, is related to how the rasterizer in this particular case works. As I noted before, fonts used to be more pixelated, i.e. there were fewer pixels and rendering fonts in a certain amount of pixels requires work for it to look right.</p>
<p>If you used Windows and Mac OS X back in the day on a low-DPI screen, you probably preferred one over the other when it came to how they handle font rendering. It all comes down to a difference in philosophy, as it turns out. In a blog post written from 2007, Joel Spolsky <a href="https://www.joelonsoftware.com/2007/06/12/font-smoothing-anti-aliasing-and-sub-pixel-rendering/">wrote</a>:</p>
<blockquote>
<p>Apple generally believes that the goal of the algorithm should be to preserve the design of the typeface as much as possible, even at the cost of a little bit of blurriness.</p>
</blockquote>
<p>This is in contrast to Microsoft, who did things differently:</p>
<blockquote>
<p>Microsoft generally believes that the shape of each letter should be hammered into pixel boundaries to prevent blur and improve readability, even at the cost of not being true to the typeface.</p>
</blockquote>
<p>On modern HiDPI displays, the old fight matters much less. High density displays give both systems enough pixels that they do not need to distort glyphs as aggressively. macOS has effectively leaned into grayscale antialiasing since the Mojave era, and Windows is all over the place because how fonts are rendered mostly depends on the software stack itself. For example, you have DirectWrite/Direct2D/Skia-style rendering, but also older Win32/GDI paths can still behave differently. ClearType tuning is also still relevant for those older apps.</p>
<p>But anyway, that’s enough for my little historical tangent. How you decide to render text on a device with limited pixels depends on your philosophy.</p>
<p>Kobo devices have a renderer that aggressively tries what we called “grid-fitting”, which seems to be similar to Microsoft’s approach, resulting in “sharper” text, sometimes at the cost of accurately displaying the various designs that are part of the actual font files.</p>
<p>Historically, that makes sense: E Ink displays used to be worse (lower resolution). But with Kobo devices these days being 200+ DPI, just rendering the outlines and not caring much about hinting or grid fitting is actually a sensible idea for a more honest representation of what a font looks like. That’s what I was aiming for.</p>
<p>Regardless, I was able to discern that it was a grid-fitting problem, and it was specific to some fonts under some circumstances. There’s an easy solution to this problem, too: simply stop grid-fitting.</p>
<p>My initial investigation led me to simply hinting the font, but that didn’t solve the issue. Sometimes fonts end up looking nicer when hinted, but the inconsistent baseline seemed to persist. I asked the LLMs to investigate, and after the “pixel inspector” was tasked to find differences and a further investigation was launched, this was reported:</p>
<p class="attributed-ai" data-provider="Claude Opus 4.8 (1M)">And here’s where iType — the specific, proprietary rasterizer on this device — does something that mainstream FreeType doesn’t: iType’s automatic grid-fitting depends on the letter’s horizontal sub-pixel position.</p>
<p>That may sound like it’s a foreign language to you, and it did to me. I asked for clarification, and this is what Claude told me:</p>
<p class="attributed-ai" data-provider="Claude Opus 4.8 (1M)">Why would horizontal position affect <em>vertical</em> height? Because of how text is laid out. As words are placed, the “pen” moves across the line and accumulates <em>fractional</em> positions — after some letters the pen sits at x = 10.0, after others at x = 10.3. When iType then grid-fits a letter sitting at x = 10.3, the rounding of its <em>top edge</em> comes out differently than for the same letter at x = 10.0. Same letter, two heights, purely because of where it landed horizontally.</p>
<p>This explains why sometimes an identical character would end up being correctly rendered on one line, and incorrectly on another. The position of where the glyph is matters when drawing and determining what pixel to snap to.</p>
<p>The fix, it seemed, was easy:</p>
<p class="attributed-ai" data-provider="Claude Opus 4.8 (1M)">Inside iType there’s a decision gate (the disassembly pinned it to address <code>0x6f302</code> in <code>libfreetype.so</code>) that reads exactly this bit: if “no hinting” is set, it skips the grid-fitter entirely and renders the raw, smoothly-scaled outline. With no snapping, every instance of a letter is geometrically identical.</p>
<p>Sure enough, this turned out to be the solution. Judge for yourself, I made a little illustrative picture that highlights where you can look to see the differences:</p>
<img src="/media/blog/20260717_233038_478649/source.png" class="kobo" alt="A before and after grid-fitting, i.e. before and after the fix. The latter uses the smoothly-scaled outlines, without attempting to snap to a particular pixel row.">
<h3>NickelHome</h3>
<p>You may recall that some time ago back in March, I was tinkering around with Kobo’s home screen.</p>
<p>I wrote about that in my blog post about KoboPatch Web UI. I wrote about how NickelMenu is basically persistent, which is what I wanted for these typesetting fixes, too:</p>
<blockquote>
<p>I believe I mentioned NickelMenu earlier. It is actually a modification that <em>persists</em> after software updates, because of how it works. It patches onto the system partition a new file that never gets overwritten by Kobo’s software packages, but can uninstall itself if it deems itself incompatible.</p>
</blockquote>
<p>I already worked on <a href="https://github.com/nicoverbruggen/NickelHome">NickelHome</a>, a standalone port of my modifications to NickelMenu, and it gave me a good starting point for hooking into more of Kobo’s software stack.</p>
<p>I wanted a durable fix, that would not vanish after installing a mod, so I decided I was going to do two things:</p>
<ul>
<li>Make a mod so every font loads correctly.</li>
<li>Integrate something into KF fonts in <code>kobo-font-fix</code> so that the correct outlines are loaded on Kobo devices even without the mod.</li>
</ul>
<p>This gives font enjoyers the solution without needing to apply a custom mod, and for type fanatics like me, a much better reading experience.</p>
<h3>NickelTypeFix</h3>
<p>Most mods just hook into <code>libnickel</code>, the main library driving Kobo’s software stack, but there are others you can hook into. In order to make this work, I’d need to hook into some other libraries.</p>
<p>Anyway, for a complete technical breakdown, I recommend reading through the documentation in the repository of the mod that I created to address this and a few other problems. The mod is called <a href="https://github.com/nicoverbruggen/NickelTypeFix">NickelTypeFix</a> and it aims to fix these issues:</p>
<ul>
<li>The baseline alignment issue (1 pixel glyph shift, the subject of this blog post).</li>
<li>Text justification.</li>
<li>Letter spacing not being applied to spaces.</li>
<li>Capital spacing (cpsp) being applied incorrectly.</li>
<li>Vertical CJK text rendering incorrectly.</li>
<li>The reader incorrectly rendering fonts with a number in the name.</li>
<li>The book falling back to the system font.</li>
</ul>
<p>In particular, the baseline alignment issue was driving me mad, and that’s the one that I find the most impactful. If you’re curious, the repository contains more information about the various fixes and how the issues were fixed.</p>
<p><strong>The point of this mod specifically is to fix issues, which is allowed in the EU.</strong></p>
<p>Two further notes:</p>
<ul>
<li>The mod will probably cease to be useful when the 5.x software release happens.</li>
<li>Having a fixed renderer is an improvement for my personal reading experience, as I have purchased over 150+ books via the Kobo Store.</li>
</ul>
<p>I also made <a href="https://github.com/nicoverbruggen/NickelDissolve">another mod</a> that emulates the animation that’s also on Kindles. I really like it. It probably deserves a dedicated blog post, too.</p>
<h3>Enhancing <code>kobofix.py</code></h3>
<p>I decided that I also wanted to bake something into the <code>kobo-font-fix</code> repository, if possible. Doing so would enable all KF font variants that I distribute to benefit from this fix. To recap what causes the problem:</p>
<p class="attributed-ai" data-provider="Claude Opus 4.8 (1M)">A font with no per-glyph instructions falls into iType’s automatic grid-fitting, which snaps each glyph’s top to a whole pixel row depending on its sub-pixel position, so the same letter renders at slightly different heights from one place to the next, which looks like an uneven baseline.</p>
<p>Notice how it says <em>a font with no per-glyph instructions</em>. After learning that font hinting does affect rendering, I wondered, and asked myself the question: could we bake hinting instructions into the font that effectively say: don’t do anything? That would avoid triggering the automatic grid-fitting path:</p>
<p class="attributed-ai" data-provider="Claude Opus 4.8 (1M)">iType routes a glyph that carries any instructions through its interpreter instead, bypassing the auto-grid-fit. So the KF preset removes global hinting tables and replaces every outline glyph’s bytecode with a single one-byte no-op program (<code>SVTCA[Y]</code>, opcode <code>0x00</code>) that moves no points: the outline is emitted byte-for-byte unchanged, but iType now renders the raw scaled outline (unmodified).</p>
<p>Sure enough, this fixes the baseline issue conclusively on custom fonts, but doesn’t fix any of the other problems that the mod fixes, but it’s a quick win for everyone who loves having <a href="https://github.com/nicoverbruggen/ebook-fonts">good typography</a> on their e-reader.</p>
<p><em>To be continued…</em></p>
<style>
figure, picture {
width: 100% !important;
display: inline-block !important;
text-align: center !important;
}

.kobo {
width: 502px;
margin: auto;
border: 1px solid #888;
filter: saturate(0.45) contrast(0.85) brightness(1.1) sepia(0.1);
image-rendering: auto;
}

.cta {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}

.post .content a.big-button {
text-decoration: none;
margin: auto;
padding: 7px 15px;
font-weight: bold;
background-color: var(--primary-color);
color: #FFF;
border: none;
border-radius: 15px;
margin: 20px 0px;
}
</style>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:jailbreak" role="doc-endnote"><p>The idea of jailbreaking your device is not even a thing with these devices. You just need to change a file on the filesystem and you can easily enable SSH. At that point, you’re essentially free to do with your device what you want. Well, within reason, of course. But it’s effectively way less locked down than, say, a Kindle, which does require a jailbreak.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:jailbreak" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:windows_ripoff_fonts" role="doc-endnote"><p>In case you didn’t know: Microsoft wasn’t keen on paying Linotype’s licensing fee for their fonts, so various similar typefaces were created that were identical in terms of layout. This is why and how Arial, Times New Roman and various other fonts were created. Font enthusiasts will often prefer the original Linotype fonts. For example, see Mark Simonson’s <a href="https://www.marksimonson.com/notebook/view/the-scourge-of-arial/">The Scourge of Arial</a> written in 2001, when Arial was taking over as the default font in Microsoft Office. In 2001, 72 dpi screens were the norm, so Mark wrote: “The situation today is that Arial has displaced Helvetica as the standard font […] This is not such a big deal since at the low resolution of a computer screen, it might as well be Helvetica.“By comparison, Apple has always had ties to the desktop publishing industry and they always shipped proper fonts with their operating system.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:windows_ripoff_fonts" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:derivative" role="doc-endnote"><p>Readerly isn’t that special, because I never did any manual editing on it. It was simply based on an existing font and various reproducible transformations were applied to the original variable font. Libron, on the other hand, has been a project that I’ve been working on a ton with lots of manual editing.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:derivative" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:monotype" role="doc-endnote"><p>Given that I’ve theorized that Rakuten Kobo have ended their licensing deal with Monotype given the disappearance of their fonts on the Kobo Libra Colour, Kobo Clara Colour and Kobo Clara BW, I wonder what they will do in the future with font rendering on firmware 5.x. I don’t have many good things to say about the so called ‘accessibility preview’ (opt-in build) that you can install if you’re using a recent Kobo device in Europe. It’s slower and the text looks worse. Given that the primary activity on a Kobo is <em>reading</em>, I’d say this is pretty unforgivable. I can only hope that they properly fix the issues. There’s also an <a href="https://www.mobileread.com/forums/showthread.php?t=361035">interesting thread</a> with various observations on the MobileRead forums, of course.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:monotype" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:swap" role="doc-endnote"><p>The engine swap was an interesting experiment, but the actual text rendering afterwards was better in some ways, this meant that the CSM system which is governed by the font weight slider in the Advanced text panel doesn’t work. I tried getting that working, but I felt the results were not good, plus the font slider quickly impacted the font rendering of the UI due to how the hooking mechanism works, so I abandoned this experiment.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:swap" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sat, 18 Jul 2026 12:00:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[Crafting Libron with FontForge]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/libron" />
        <id>https://nicoverbruggen.be/277</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p class="headline">A while ago, I announced that I was working on Readerly, a new font derived from Newsreader. It was mostly built with an interactive build script and a bunch of manual testing on my part.</p>
<p>The script for Readerly in question uses Python, FontForge’s Python API and various tools to modify the font, and some knobs (weight, scaling, etc.) that decide what the final font looks like. Of course, I also made sure to integrate <code>kobofix.py</code> into the build script, and recently added a web font build, as well; you can download a WOFF2 version, too.</p>
<p>It’s a decent attempt at a tweaked font, and I was pretty happy with the result so far. But it was not a <em>hand-made</em> font. Since I’ve been using it as my go-to font for quite some time now, I do find some faults with it.</p>
<h3>Improving upon Readerly</h3>
<p>In particular, I wasn’t a fan of Newsreader’s heavy serifs. They seemed sort of… distracting, especially for body text. I looked into manually patching individual glyphs for Readerly, but before I knew it, I was effectively working on a new font with more than a handful of individual patches.</p>
<p><img src="/media/blog/20260606_161729_584521/source.png" alt="A good example of an adjusted serif in Libron versus Readerly; the capital G. Displayed here in my custom tool, Font Comparator." /></p>
<p>At that point I decided a fork was in order. I built a web tool that lets you compare individual glyphs between two fonts and diff them, and voila. I now had an excellent foundation to get started with some manual editing in FontForge.</p>
<p>I knew it’d be a time-consuming process, but probably worth it. I’ve tentatively called the resulting font <em>Libron</em>.</p>
<p><img src="/media/blog/20260606_161758_346386/source.png" alt="Font Comparator includes a helpful diff view for individual glyphs, that will let you see, at a glance, what parts of glyphs differ between two fonts." /></p>
<p>Libron addresses some of those complaints with the original Readerly. In order to make it, I started with Readerly as the basis, and I’ve been tweaking the prototype font with FontForge for the last few days, with great success.</p>
<p>After a lot of manual tinkering, and learning how to really use FontForge, I’ve come up with an initial version that I’m currently testing on my Kobo e-reader. So far, I think I prefer it over Readerly.</p>
<p>In this blog post, I’d love to take you through the journey of building it, all while imparting advice on how to use various font tooling.</p>
<h3>Patching glyphs</h3>
<p>My initial instinct was to simply apply individual patches to glyphs. There’s a few in the latest release of Readerly, but their nature is very limited. Only <code>b</code>, <code>d</code>, <code>s</code> amd <code>u</code> were patched, specifically with fixes to address the baseline rendering.</p>
<p>However, I found generating patches like these to be a frustrating process. I generated the patches with the help of manual edits and getting large language models to extract the relevant diffs into a sort of patch set.</p>
<p>These patches would allow me to further customize Readerly, while still starting from the variable font; that’s where the configurable knobs are a definitive advantage versus manually editing glyphs, of course.</p>
<p>After this, I wondered: would maintaining a manual fork (a separate font) be the best way to get those edits into Readerly?</p>
<p>I could start from Readerly Regular as the basis, work on it, extract what I modified from that, and integrate the modified glyphs into the base font as a post-processing step.</p>
<p>It had not been a bad thought, but that separate font quickly grew beyond a simple fork from which I was going to extract modified glyphs to feed back into Readerly. As I saw more potential areas of improvement, the scope of the changed glyphs changed rapidly.</p>
<h3>A separate font</h3>
<p>Am I really going to integrate all of these changes into Readerly? To be perfectly honest, I don’t know. I may still do so in the future, perhaps under another name. I’ve named this new forked version <strong>Libron</strong>.</p>
<p>As far as the etymology of that word is concerned… <em>libro</em>, is, of course, the Spanish word for <em>book</em>.</p>
<p>I’ve got a fitting name, then, for a font that’s stylistically somewhere in the middle of Newsreader and Bookerly, no? I like to think so, at least.</p>
<img class="type" src="/media/blog/20260608_212830_241135/source.svg" alt="Like Readerly, I created a quick specimen file like the one you might find for most fonts on Wikipedia.">
<h3>How I used FontForge</h3>
<blockquote>
<p>FontForge has <a href="https://fontforge.org/docs/index.html">extensive documentation</a> available that explains all of the available features. I also recommend <a href="http://designwithfontforge.com">Design with FontForge</a>.</p>
</blockquote>
<p>There are a few important tasks I needed to undertake in order to build Libron. First off, adjusting the metadata of the various fonts is required if you plan on making a new font.</p>
<p>This includes resetting version data, updating the copyright information, randomizing the font’s UID, and such. This is important, otherwise you risk the font showing up with the name of the original, which is not intended.</p>
<h4>Glyph modifications</h4>
<p>Fonts are, generally, nothing more than a series of outlines for individual character designs (“glyphs”) that are, effectively, vector based; these can be mathematically expressed and scaled.</p>
<p>I knew that I was going to make modifications to the uppercase characters, so I needed to adjust the vector outlines here of, for example, the uppercase C. You can see what the new outline looks like here:</p>
<p><img src="/media/blog/20260606_162814_466320/source.png" alt="I removed the serif at the bottom of this particular glyph." /></p>
<h4>Bearings and kern pairs</h4>
<p>In addition to editing glyphs, another aspect of making a good font has to do with the left and right bearings for individual glyphs. Bearings dictate how much room is left between individual glyphs, and dictates how tightly spaced the individual letters are.</p>
<p>Getting these metrics right is very important. This isn’t necessarily something you can easily automate. It takes trial and error to determine what works best optically for the design of your particular font.</p>
<p><img src="/media/blog/20260606_162830_419370/source.png" alt="This is the metrics screen for glyphs. It displays the width of each glyph, as well as bearing metrics and kern pair (or class) information." /></p>
<p>In addition to broad adjustments in terms of metrics, you can also tweak individual glyph combinations with kerning, which is an additional modifier that is applied in addition to the default tracking.</p>
<p>Because it would be silly to manage all of these individual kern pairs, there’s also something called kern classes, which allows you to group similar glyphs together so you can apply consistent kerning to a group of characters.</p>
<p><img src="/media/blog/20260606_162836_550585/source.png" alt="Kerning by class allows you to group together related glyphs, instead of needing to maintain many of the same kern pairs. These pairs are part of the GPOS table." /></p>
<p>For the most part, I’ve inherited existing kern pairs from Newsreader and Readerly, but in the future some changes may be required due to the nature of the outline changes to the font. Some glyphs are now more narrow, which has an optical impact and requires changes to tracking and kerning.</p>
<h4>Build script</h4>
<p>That’s all I do with FontForge. I make sure the font is mostly ready for immediate use. The FontForge files are the “source” files that are then processed and exported. I say processed, because I have a build script, like with Readerly.</p>
<p>This build script then uses a <a href="https://github.com/nicoverbruggen/fntbld-oci">custom container image</a> called <code>fntbld-oci</code> which I built specifically to have access to an easy build system that has Python, common Python packages for font checks, validation and FontForge’s CLI, and uses all of these bits to generate the release, which includes the regular and web fonts.</p>
<h3>Download</h3>
<p>You can find <a href="https://github.com/nicoverbruggen/libron">Libron</a> on GitHub. It’s still a work in progress, so it will likely be updated frequently over the next few weeks. Once I’m happy enough with the font, I will tag that as version 1.0 of the font.</p>
<p>If you would like to download Libron so you can use it on your e-reader, you have a few options. You can:</p>
<ul>
<li>Download the latest release for <a href="https://github.com/nicoverbruggen/libron/releases/latest/download/KF_Libron.zip">Kobo devices</a> directly.</li>
<li>Download the latest release for <a href="https://github.com/nicoverbruggen/libron/releases/latest/download/Libron.zip">other e-readers</a> directly.</li>
</ul>
<p>Currently, Libron is not yet included in my <a href="https://github.com/nicoverbruggen/ebook-fonts">eBook Fonts</a> repository. It will be added at a later date, once version 1.0 is ready.</p>
<h3>Acknowledgements</h3>
<p>Like last time, I would like to acknowledge the original type designers from <a href="https://productiontype.com/">Production Type</a> who worked on Newsreader and created a very beautiful font.</p>
<p>I would also like to reference my previous work on <a href="https://github.com/nicoverbruggen/readerly">Readerly</a>, as well, since it formed the basis for this particular modification.</p>
<style>
figure, picture {
width: 100% !important;
display: inline-block !important;
text-align: center !important;
}

.kobo {
width: 500px;
margin: auto;
border: 1px solid #888;
}

.type {
width: 500px;
margin: auto;
border: 1px solid #888;
}

.bordered {
border: 1px solid #888;
}
</style>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Mon, 08 Jun 2026 10:30:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[Great Expectations]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/great-expectations" />
        <id>https://nicoverbruggen.be/276</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p><em>This blog post is unrelated to the book by Charles Dickens, in case you were curious. I was just being a bit cheeky when I came up with a title for this one.</em></p>
<p>I think there’s a lot of societal expectations placed upon people these days in a way that concerns me: in particular, one of the most burdensome pressure that I’ve faced over the last few years is the idea that we should chase happiness.</p>
<p>It’s totally understandable that we want to be happy. After all, it’s what a lot of us live for. There are many beautiful things to life. But reality always comes with unhappy experiences, too, many of which are unavoidable as they are part of life.</p>
<h4>Fruitful work</h4>
<p>The last few years haven’t been that great of an experience for me in my personal life. Between relationship struggles, losing family members, friends moving away, working hard on my physical health, dealing with sleep apnea and the burnout that it caused… it’s been a challenging few years in stark contrast to the years that came prior. (I even <a href="https://nicoverbruggen.be/blog/habitual-improvement">wrote</a> <a href="https://nicoverbruggen.be/blog/character-development">about</a> <a href="https://nicoverbruggen.be/blog/thoughts-on-taking-a-break">a bunch</a> <a href="https://nicoverbruggen.be/blog/burnout">of</a> <a href="https://nicoverbruggen.be/blog/sleep-quality">these</a>.)</p>
<p>Compared to my twenties, I’m carrying a lot more emotions with me in my thirties, many of which I can now identify correctly. Of course, this was at its worst when I crashed due to burnout, but that was mostly caused by disturbed sleep and stress (which often accompany or cause depression).</p>
<p>That being said: the work that I’ve done has been fruitful. All of this work has caused significant change within me, and not in minor ways.</p>
<h4>Living in the moment</h4>
<p>With all of that, the pressure has been high to be happy, and to feel like I’m doing well. The idea of wanting not to be “too far behind” what other folks of my age are doing, is something I hadn’t experienced prior to a few years ago.</p>
<p>It may be true that everyone does things at their own pace, but that doesn’t mean you won’t lose touch with people who have entered different stages of life; it can also be very painful if you cannot have what you truly want, but people close to you don’t share your struggle.</p>
<p>Part of the pain is the unfortunate reality of the wandering mind: as you ponder the world that is, was and can be, you are likely to bump into some uncomfortable or sad realizations. So, ironically, pondering your own happiness is more likely to make you unhappy than happy. As such, I don’t recommend doing it too often.</p>
<p>It’s a lesson that took a while for me to properly take in. Being in the moment, and making sure that your average, say, weekday, is properly enjoyable and rewarding is a far more important thing to look into. Especially living in the moment.</p>
<h4>Sitting with uncertainty</h4>
<p>Currently, I’m reading and learning more about Buddhism, which I find quite interesting. I’m convinced I can learn useful lessons from some of the teachings of eastern spirituality, because it is much more inward-faced.</p>
<p>This <em>internal state</em> is a fascinating topic and not sufficiently the topic of conversation here in the West; the external world is generally considered to be more important here. Neglecting our internal state can have severe consequences, though.</p>
<p>I’m also trying to get more comfortable dealing with uncertainty. I find sitting with uncertainty to be quite challenging, and I hope it’s something I can get better at in the future, because it’s a very common issue when dealing with things beyond your control. We like to hold on to things, but sadly, everything is transient.</p>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sun, 31 May 2026 12:00:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[Onboarding for PHP Monitor]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/phpmon-onboarding" />
        <id>https://nicoverbruggen.be/275</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p><a href="https://phpmon.app">PHP Monitor</a> was originally intended to be a tool created for use with Laravel Valet, but over time it has definitely changed a lot. Many features were added, and it has even expanded to also support a standalone mode without Valet.</p>
<p>One thing has been consistent all this time, however: PHP Monitor has always required a little bit of manual setup before you can start using it. You need both Homebrew and PHP to be installed, and your PATH to be configured correctly, after all. That takes a little bit of work.</p>
<p>All in all, it’s not super difficult, but it’s definitely a bit of a hurdle for new users today, and it’s something I’ve remedied with the latest update to what used to be a simple toolbar app. At this point, it’s more than just that.</p>
<h3>Valet as the default</h3>
<p>Back in 2020, most people were using Valet as it was the go-to way to get started with Laravel. The setup was solid: you installed various dependencies as part of Homebrew, you installed Valet (a CLI app) via Composer, and PHP Monitor provided you with a pretty easy to use user interface.</p>
<p>The most common use case was simply installing PHP Monitor alongside the development environment that you already had set up. But it was not hassle-free. The nature of Homebrew’s linked libraries as such was that many folks did not like how long it took to install updates for dependencies; and not keeping dependencies up-to-date meant there was an increased chance of a binary no longer working correctly. If it was PHP, well, your Valet setup would be broken.</p>
<p>It’s why PHP Monitor came along and gained in popularity – it’d monitor your Valet setup, and gave you easy access to your sites and projects via a simple GUI. If anything went wrong and your install was broken, the app identifies the issue and tells you what commands to run in your terminal to get things working again.</p>
<h3>The landscape changes</h3>
<p>A few years later, <a href="https://herd.laravel.com/">Laravel Herd</a> came along with <a href="https://php.new">php.new</a>, which is heavily pushed by the Laravel organization and Beyond Code as the first-party and best way to set up your development environment. This ships a static version of PHP and is a bit more portable.<sup id="fnref:static"><a class="footnote-ref" href="#fn:static" role="doc-noteref">1</a></sup></p>
<p>It’s nice, but I personally think it’s more convenient to manage all your dependencies with Homebrew. It feels cleaner somehow. I am, of course, a little bit biased here.<sup id="fnref:homebrew"><a class="footnote-ref" href="#fn:homebrew" role="doc-noteref">2</a></sup></p>
<p>So ever since these came into being I wondered if it was possible to improve the onboarding experience for PHP Monitor, so it can remain a viable alternative. It’s true that Herd is first-party way to do things, but PHP as a whole is broader than Laravel, and not everyone is using Laravel Herd. There’s plenty of other options.</p>
<p>It’s actually for this reason that I originally called my app <strong>PHP</strong> Monitor, because I know there’s a whole world of developers out there which are building projects with other PHP-based frameworks or CMS systems which are unaffiliated with the Laravel organization.</p>
<p>Plus, even if you use Laravel, Valet still remains supported and I know plenty of developers still use it. It’s for those developers that I still maintain and update PHP Monitor (and also for myself).<sup id="fnref:valet"><a class="footnote-ref" href="#fn:valet" role="doc-noteref">3</a></sup></p>
<h3>Onboarding for new users</h3>
<p>Anyway, that’s a little bit of history.</p>
<p>I hope it helps you understand why it took me so long to get here. It’s because of these historical reasons, the app has been light on the onboarding experience for new users, because it was intended for users who already used Valet.</p>
<p>In today’s release, that changes. In this new update, PHP Monitor has received a new onboarding experience. As such, I’ve internally dubbed this month’s release the <em>Big Onboarding Update</em>, and I’ve been calling it that for a while now.</p>
<p>I’m glad it’s finally here!</p>
<p><img src="/media/blog/20260606_160345_312951/source.jpg" alt="The new Setup Assistant can help you set up your Mac with a few easy steps. If you’ve already got Homebrew installed, it will take about a minute to get ready. If not, it will take a little bit longer." /></p>
<p>The onboarding flow is especially important because I suspect there are many new users who try PHP development. These folks are probably going to be finding their way to Herd or PHP Monitor if they’re looking for a GUI and something lightweight.</p>
<p>I hope that this “new user experience” may help PHP Monitor find a new or broader audience.</p>
<p>After all, convenience does matter. That’s been made abundantly clear to me over the past few years. It’s part of the appeal of PHP Monitor, and of some of the other projects I’ve worked on in the past few years. People will use stuff that makes their life easier and less complicated.</p>
<p>I hope that PHP Monitor can continue to keep doing so for the foreseeable future.</p>
<p>The new onboarding functionality I described above, and some other minor additions are now available with the arrival of version <a href="https://github.com/nicoverbruggen/phpmon/releases/tag/v26.05">26.05</a>.</p>
<h3>Updated website</h3>
<p>I also pushed an update to the website of the app. You may notice that what used to be PHP Monitor’s unofficial tagline… is now the official tagline. I hope this explains why the icon of the app has been a truck all these years!</p>
<p>The <a href="https://phpmon.app">website</a> has also been updated to mention the Setup Assistant, and it’s also been restructured somewhat to reflect the current nature of the app. A small FAQ is now also present on the one-pager.</p>
<style>
.post .content figure > picture > img, .post .content picture > img {
border: 1px solid #CCC;
}
</style>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:static" role="doc-endnote"><p>Without a doubt, there are some advantages to a static version of PHP. It’s easier to update since there are no linked dependencies, and it can be faster because those linked dependencies do not need to be loaded. If there’s enough cache (as there is on the Mac with Apple silicon) it can be a tad faster. I’ve thought about shipping PHP Monitor with static binaries but I decided against it since that’d just make it worse. (Unless I ship it as an additive feature; i.e. you can can use Homebrew OR an embedded version of PHP. That’d work, but it’s too much work for too little gain at this time.)&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:static" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:homebrew" role="doc-endnote"><p>A static version of PHP is an isolated version of PHP that is managed externally from all the other potential dependencies that you should probably be installing via Homebrew anyway. For example, the go-to method to install the latest version of Python, Rust or Go on a Mac doesn’t involve downloading static binaries from a third party. If you go to php.net and visit the <a href="https://www.php.net/downloads.php">download page</a> you will see that they recommend using Homebrew on the Mac.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:homebrew" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:valet" role="doc-endnote"><p>I don’t think Laravel will deprecate Valet, but if they ever do that, I will start working on a drop-in replacement for Valet (albeit without integration for the “share” functionality that is currently built-in) so that PHP Monitor can continue to work as expected.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:valet" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sat, 06 Jun 2026 12:00:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[Self-Hosting Git]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/self-hosting-git" />
        <id>https://nicoverbruggen.be/274</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>A couple of years ago <a href="https://nicoverbruggen.be/blog/moving-to-nextcloud">I started using Nextcloud</a> instead of Dropbox after having moved away from Gmail, and it was such a novel and good experience not to be constantly bombarded with bullshit, that I decided to slowly move my life away from the big businesses and their annoying practices.</p>
<p>Most recently, I wrote about <a href="https://nicoverbruggen.be/blog/goodbye-windows">getting rid of Windows</a> but I’ve also been moving away from other products in favor of some self-hosted options, which is now something that’s fairly easy to set up these days.</p>
<p>In this particular blog post, I want to discuss where I’ve been hosting my personal Git repositories over the years. It’s a fairly timely post, I think, with lots of discussion about GitHub’s reliability due to an exponential influx of code on the platform lately.</p>
<h3>GitHub and GitLab</h3>
<p>Ever since GitHub was acquired by Microsoft, they’ve been heavily pushing Copilot and the platform has generally become <a href="https://mrshu.github.io/github-statuses/">less reliable</a> due to frequent outages. I understand that due to the rise of large language models (“AI”) usage of the platform has skyrocketed, so this is not a huge surprise.</p>
<p>I hear more and more folks clamoring for the platform to be improved, while others are seeking or pursuing alternatives that already exist. I personally still use GitHub for my open source work, but I’ve been hosting my private repositories elsewhere for quite some time now.</p>
<p>You may not remember, but GitHub used to not allow private repositories unless you paid for the product. Because of this, I had been using GitLab.com for some time, as I was familiar with it because of work: we used a private GitLab instance at the company where I worked for over a decade. So, ever since 2015, I’d been pushing to GitLab and using their workflow infrastructure for my projects.</p>
<p>In 2019, GitHub made <a href="https://github.blog/news-insights/product-news/new-year-new-github/">some changes to the platform</a> which made private repositories available for everyone (with some restrictions), but I kept happily using GitLab during this time.</p>
<p>But at some point GitHub started getting worse after the Microsoft acquisition, and even GitLab.com started pushing AI features. That’s when I decided I was going to need to come up with an alternative solution that did not involve giving money to vendors who are actively making their platform worse with unneeded “AI features”.</p>
<p>I had also tried to self-host GitLab, but the system requirements were rather high for what I was trying to accomplish with my self-hosting effort. So, at the time, I decided to self-host Gitea, as an experiment.</p>
<p><em>From this point on, I will refer to these as <em>Git forges</em> because there’s these three different projects that share a history: <a href="https://gogs.io">Gogs</a>, <a href="https://about.gitea.com/">Gitea</a>. and <a href="https://forgejo.org/">Forgejo</a>.</em></p>
<h3>Requirements</h3>
<p>In order to be able to properly move away from GitHub as the primary platform, I established that following requirements for my use case were relevant:</p>
<ul>
<li><strong>A private instance with some access control</strong>: You have to be able to limit access to this crucial piece of infrastructure, or at least have access control for certain content stored on the instance.</li>
<li><strong>Full data control and data sovereignty (self-hosted)</strong>: In a world where self-hosting and having control of your own data is no longer the norm, this is pretty important if you don’t want your data to end up everywhere.</li>
<li><strong>As little lock-in as possible</strong>: While Git repositories are free of lock-in, issues, projects, and various other organizational metadata is generally not portable.</li>
<li><strong>Repositories, organizations, issues, pull requests</strong>: Sure, it might be just the basics, but unless you make a simple web GUI for <code>git</code>, you need all of these things if you plan to get work done in a team or as a solo developer with some sense of structure.</li>
<li><strong>Under active development and open source</strong>: This one is fairly self-explanatory. If something is no longer being maintained, it’s a massive security risk. If it’s open source, it’s likely to be affordable or even fully open, which gives you the ability to apply customizations if you so choose.</li>
<li><strong>Acceptable or limited system requirements</strong>: There’s a wide range of system requirements depending on what it is that you’re trying to run. For smaller teams, this can save a lot of money and prevent slowdown.</li>
<li><strong>Support for workflows with runners</strong>: GitHub Actions is pretty cool, and quite important. You want to be able to use something functionally similar if you’re serious about software development.</li>
</ul>
<p>If I had to set something up from scratch today, I would likely go with Forgejo because it’s getting adopted by e.g. <a href="https://forge.fedoraproject.org">Fedora</a> and the <a href="https://code.overheid.nl">Dutch government</a>.</p>
<h3>Mirrors</h3>
<p>So, since late 2022, I’ve been using Gitea to maintain mirrors of some of my most important repositories (and, of course) <a href="https://phpmon.app">PHP Monitor</a>. Gitea has a very easy to use “mirroring” feature, and it works both ways.</p>
<p>Every few hours, Gitea will attempt to fetch the latest commits from the remote, and update the copy on my VPS. This was a pretty good strategy to ensure I always had an easily accessible and backed-up copy of all of my repositories.</p>
<p>In 2024, after setting up a proper incremental backup strategy (which we’ll get into later) my private Gitea instance became my main “remote” that I’d use, with mirroring now in the opposite direction (with pushes).</p>
<p>(For example, when I work on PHP Monitor, I am usually pushing to my own server, which then immediately mirrors those changes by pushing to GitHub.)</p>
<p><img src="/media/blog/20260504_011422_064667/source.png" alt="When I push commits to the early access repository, it’s automatically pushed to the private repository on GitHub for people who sponsor me." /></p>
<p>This means that my public contributions are still accessible via GitHub since they’re automatically mirrored, but I also know I benefit from the reliability of having access to my own Git remote that only goes down when I say so. (The outages at my VPS host are far less frequent than the outages at GitHub, let me tell you.)</p>
<h3>Customizations</h3>
<p>There’s a couple of <a href="https://docs.gitea.com/administration/customizing-gitea">customizations</a> I’ve applied to my Gitea setup, because I really felt like the default look and feel was a bit bland.</p>
<ul>
<li>I now use <a href="https://www.brailleinstitute.org/freefont">Atkinson Hyperlegible Next</a> as the main font instead of the default system font. This gives the website a more uniform look on various operating systems, and makes it easier to read.</li>
<li>Blue accents are now present throughout the entire light theme, as opposed to the normal gray look.</li>
<li>The logo, favicons and theme color have been adjusted.</li>
<li>In order to make it look more like GitHub, the heatmap feature in the activity feed now uses the classic green colors you may be familiar with.</li>
<li>I’ve added a custom welcome page for anonymous visitors with a helpful explanation what this website is trying to accomplish.</li>
<li>Custom links have been added to the navigation bar and the footer.</li>
</ul>
<p><img src="/media/blog/20260504_110747_508087/source.png" alt="What my own Git forge currently looks like. I’ve applied various customizations and some theme changes to make sure things look good. You can also see in the activity feed that my public PHP Monitor repo synced commits in the background." /></p>
<h3>Forgejo</h3>
<p>I should mention that I missed <a href="https://forgejo.org/2024-02-forking-forward/">the hard Forgejo fork in 2024</a>, probably due to a mix of burnout and not paying attention during that time. Forgejo’s hard fork is mostly related to the governance of the project.</p>
<p>Because there were disagreements about that, the hard fork was decided upon and both projects have now diverged in terms of priorities and progress, with both still being quite similar.</p>
<p>If I were setting this up from scratch today, I’d probably use Forgejo. They also dogfood their own work: they run their entire development process on <a href="https://codeberg.org/forgejo/forgejo">Codeberg</a>, which is based on Forgejo. If you decide to pull in the Docker image, it’s hosted on Codeberg, too.</p>
<p>Migrating to Forgejo used to be quite easy since there was a seamless migration path you could take if you ran Gitea, at the time. Unfortunately, unless you are running this older version of Gitea, this migration path is no longer available today.</p>
<p>I also automatically run CI and deploy various projects via the <a href="https://docs.gitea.com/usage/actions/act-runner">Act Runner</a> and <a href="https://docs.gitea.com/usage/actions/overview">Gitea Actions</a>, so that’d probably take some more time to migrate as well.</p>
<p>Due to these factors, I’ve decided to stick with Gitea (for now).</p>
<p><img src="/media/blog/20260504_182554_188362/source.png" alt="I made a migration script with the help of some LLMs, and I was able to migrate the majority of my data. Some data, like Actions data, cannot be migrated due to incompatibilities. I did disable the customizations that (mostly) work here as well, minor tweaks may be required for optimal results." /></p>
<p>I do plan on eventually migrating to Forgejo, but I’ll be doing that when I eventually end up setting up a new server. I did a little bit of tinkering on <a href="https://github.com/nicoverbruggen/gitea-to-forgejo">a migration project</a> which seems quite promising based on the results.</p>
<p>I should note that I’m <a href="https://msfjarvis.dev/posts/migrating-from-gitea-to-forgejo-the-long-way">not the only one</a> who has tried to do this migration as of late.</p>
<h3>Workflows</h3>
<p>Without the ability to add workflows, going for a self-hosted solution like this might seem silly. Thankfully, both Gitea and Forgejo have support for <a href="https://docs.gitea.com/usage/actions/overview">Actions</a>, which allows you to write your own workflows.</p>
<p>I should note that <a href="https://forgejo.org/docs/next/user/actions/reference/">Forgejo</a> works slightly differently (they did their own implementation) and I have the impression their Actions implementation and documentation is potentially better.</p>
<p><img src="/media/blog/20260504_011233_531946/source.png" alt="Gitea and Forgejo both allow you to set up runners and run workflows as part of the repository. Pictured here: an example deployment flow for my personal website. Both the security audit and the test suite need to pass before the deployment occurs." /></p>
<p>I use workflows to run automatic tests and deploy my projects. I have four runners on my server, and while they’re not super fast, they do the job.</p>
<p>The runners also automatically get access to certain credentials when provisioned (because I control them) which allows me to simplify some jobs, since it’s only my jobs that run on these runners.</p>
<h3>Backups</h3>
<p>Every night (when I’m generally not using the instance) the Gitea instance is stopped for a few seconds, and a full backup “dump” happens.</p>
<p>This creates a <code>zip</code> file with a full dump of all the relevant data. This includes repositories, packages, assets, database dump, and so on. That’s enough to restore the full instance at any point since I don’t have assets located elsewhere.</p>
<p>After that, an automatic backup is created with <code>restic</code> on a separate storage drive which is later duplicated to my Raspberry Pi on my local network. I really enjoy using Restic. I should write a post on that at some point, honestly.</p>
<p>I make sure to keep an archive of 7 daily, 4 weekly, 12 monthly and unlimited yearly backups with Restic. To make sure incremental backups are possible, the <code>zip</code> is first unzipped and then backed up.</p>
<p>I have a helpful script in case I want to make a manual backup, too. This is a streamlined version without additional logging, and dumps that backup in a particular fixed location:</p>
<pre><code class="language-sh">#!/bin/bash
set -euo pipefail

if ((EUID != 0)); then
  echo &gt;&amp;2 &quot;Error: script not running as root or with sudo! Exiting...&quot;
  exit 1
fi

trap 'sudo systemctl start gitea' EXIT

mkdir -p &quot;/home/nico/backups&quot;

sudo systemctl stop gitea
runuser -l git -c &quot;gitea dump -c /etc/gitea/app.ini \
    --work-path /var/lib/gitea \
    -f /tmp/gitea.zip&quot;
sudo systemctl start gitea

mv -f /tmp/gitea.zip /home/nico/backups/gitea.zip
sudo chown nico:nico /home/nico/backups/gitea.zip
</code></pre>
<h3>Scrapers</h3>
<p>Unfortunately, I’ve been dealing with some trouble as of late. There’s been a <a href="https://codeberg.org/forgejo/discussions/issues/320">bit of a trend</a> where certain companies appear to be mass-scraping the contents of public Git forges running software like Gitea and Forgejo.</p>
<p>I do keep <em>some</em> repositories public on there (mostly mirrors of my open source projects) which comes in handy when GitHub goes down, so it can be a bit tricky dealing with the scrapers from various “AI” companies. In many cases, <code>robots.txt</code> is certainly <em>not</em> respected, which is a shame.</p>
<p>There are certain mitigation techniques and some are definitely necessary if you plan on self-hosting at least a partially public Gitea or Forgejo instance, unless you’d like said data to be… uh, sucked up and your server slowed down.</p>
<p>Fortunately, there are some measures you can put into place to ensure that the majority of your Git forge isn’t accessible without signing in, or by putting up a firewall or something similar to prevent public access to certain roots without a “challenge”.</p>
<p><a href="https://github.com/TecharoHQ/anubis">Anubis</a> is a good example if you don’t want to use <a href="https://www.cloudflare.com">Cloudflare</a>, and it’s what I use for my Gitea instance. In the future, I may still use Cloudflare for this, but I currently prefer as few external servers to impact access to my instance as possible.</p>
<style>
picture img { border: 1px solid #CCC; }
</style>
<h3>Considerations</h3>
<p>If this blog post is inspiring you to consider switching to your own private self-hosted forge, keep a few key things in mind:</p>
<ul>
<li>
<p>For a private instance, you definitely want to make sure user registration is disabled via <code>DISABLE_REGISTRATION</code> in <code>app.ini</code>. Even better, link your instance via an OAuth2 provider. If you open up your instance you’re setting yourself up for a world of pain and potential security risks. <strong>Don’t make a public instance unless you know what you’re getting yourself into.</strong></p>
</li>
<li>
<p>If your private instance needs to expose some public repositories, you definitely want to throttle or limit public access to your Git forge via Cloudflare or Anubis. If not: prepare to be scraped, which isn’t good for your server load.</p>
</li>
<li>
<p>GitHub Actions aren’t entirely compatible, so you may need to make some changes to your workflows to make sure they are compatible.</p>
</li>
<li>
<p>Some maintenance is required to make sure runners are functional, your instance is updated periodically, and things keep running smoothly. You will want to keep an eye on disk usage, too.</p>
</li>
<li>
<p>Make sure you have a good backup strategy. Test your backups. It’s easy to spin up another Docker container to test if your backups work, don’t skip this.</p>
</li>
</ul>
<h3>Recommendations</h3>
<p>Migrating to your own Git forge doesn’t mean you need to use it for everything. I generally recommend that you migrate slowly, with a few repositories at first. If you’re happy with the way things work, you can migrate some more.</p>
<p>You can always set up mirroring in both directions in case a repository needs to be available on another platform like GitHub, so you have no excuse unless it’s too costly for you to run the server.</p>
<p>(A small VPS is sufficient for a small team, but I don’t have a lot of experience with how these forges scale for a huge amount of users, so keep that in mind.)</p>
<p>Happy hosting!</p>
<p class="label section">Update</p>
<p class="label_aside">May 20, 2026</p>
<p>A few hours ago GitHub <a href="https://xcancel.com/github/status/2056949168208552080">announced</a> that many of their internal repositories were compromised. While it’s too soon to see if there’s any major fallout from this particular incident, I think it makes sense to elaborate on security a bit.</p>
<h3>Security Recommendations</h3>
<p>Running your own git forge, whether it’s Gitea or Forgejo, obviously comes with various risks. If you’re managing your own infrastructure you need to make sure your environment is secure.</p>
<p>For a partially public instance, you may want to consider that:</p>
<ul>
<li>you have limited administrative access to your instance (preferably by setting <code>DISABLE_REGISTRATION</code> to <code>true</code> so unknown users cannot create an account and push code which can be used as a malware distribution vector)</li>
<li>you are installing security updates in a timely fashion (e.g. with <a href="https://ubuntu.com/server/docs/how-to/software/automatic-updates/">unattended upgrades</a> for security patches and ensuring you install security updates for Gitea and Forgejo ASAP)</li>
<li>you are managing your secrets and SSH keys carefully (it’s obvious, but do not commit secrets; if you do commit them, rotate your secrets)</li>
<li>you are running Actions <a href="https://forgejo.org/docs/latest/admin/actions/security">securely</a></li>
<li>you have backups that you restore periodically to ensure backup consistency</li>
</ul>
<h3>Securing Actions</h3>
<p>You can do even more like limiting access to your instance via a VPN or such (like a Cloudflare tunnel), which significantly reduces the potential attack vector as your instance stops being public on the internet.</p>
<p>Securing Actions runners is quite important, too: after all, in many cases projects and people are compromised via <a href="https://techcrunch.com/2026/05/19/hackers-have-compromised-dozens-of-popular-open-source-packages-in-an-ongoing-supply-chain-attack/">supply chain attacks</a> which means that auditing your dependencies is more important than ever.</p>
<p>Should something go wrong, if your Actions runners are properly isolated and secured, your risk surface is reduced significantly.</p>
<p>Ensuring that your Actions workflows themselves are secure is also important, even if you plan on staying on GitHub. Recently, Sebastian Bergmann of PHPUnit fame wrote an <a href="https://phpunit.expert/articles/hardening-github-actions-workflows.html">excellent post</a> on hardening your GitHub Actions, which I recommend checking out.</p>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Tue, 05 May 2026 15:30:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[KoboPatch Web UI]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/kobopatch-webui" />
        <id>https://nicoverbruggen.be/271</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p class="headline">I’ve been busy, so I’ve got a fun story to share today. If you’ve been following me and reading my blog, you probably know at this point that I love tinkering with my Kobo e-reader.</p>
<p>Unlike other e-readers, Kobo e-readers don’t need to be jailbroken if you want to mod them. It’s one of the best things about these devices. I like it very much: it’s customer friendly and definitely a good reason to get one!</p>
<p>In fact, if you don’t have one yet, you should probably <a href="https://www.kobo.com">pick one up</a>. (I’m just a fan, I don’t get a cut of any sales, by the way.)</p>
<p>I’ve had my various <a href="https://pgaskin.net/NickelMenu/">NickelMenu</a> <a href="https://github.com/nicoverbruggen/kobo-config">customizations</a> on GitHub for some time now, and if you’ve been keeping up with the series on typography on Kobo devices, well… you might have picked up <a href="/blog/creating-readerly">a new font</a>.</p>
<p>I love reading on these devices, but I also love tinkering with them. I may not be a low-level programmer with reverse engineering knowledge, but I do like “hacking” my devices to extend their functionality. Usually, that means <a href="https://pgaskin.net">other and smarter people</a> have done the heavy lifting for me.</p>
<p>A while ago, I discovered <code>kobopatch</code> and I never really thought I needed to manually apply such a patch on a device, until I bought a Kobo Clara BW which features – on the home screen – that third row of recommendations and advertisements, some of which cause the layout to jump.<sup id="fnref:georgia"><a class="footnote-ref" href="#fn:georgia" role="doc-noteref">1</a></sup> Oops.</p>
<p class="disclaimer split blue"><span class="emoji">👋</span> <span><b>This is the origin story of KoboPatch Web UI.</b> If you prefer to watch a video than read, there’s also a <a href="#content-updates">YouTube video</a> that I made. Not looking to learn why I made it and just want to patch your device? You can visit the website <a href="https://kp.nicoverbruggen.be?utm_source=nvb-cta">here</a>.</span></p>
<h2>Bloated home screens</h2>
<p>Now, I’m fairly confident I do <em>not</em> have OCD… but I <em>have been</em> ruined by my profession when it comes to typography, design and layouts. I tend to pay close attention to how things are built, and I simply can’t stand a bad UI.</p>
<p>I also tend to have a disgusted reaction when I encounter dark patterns that are clearly meant to manipulate people.</p>
<p>One particular example that immediately comes to mind is Kindle’s home screen – half of which is, for the most part, one big ad for their storefront. I <em>immediately</em> bounced off of Kindle for that very reason, despite the hardware and the reading experience being totally fine.<sup id="fnref:amazon"><a class="footnote-ref" href="#fn:amazon" role="doc-noteref">2</a></sup></p>
<p>I mentioned this earlier in my blog post <a href="/blog/goodbye-windows#content-the-wrong-approach">about moving to Linux</a>, but I very strongly believe that <strong>we should not tolerate user interface patterns that do not respect us</strong>.</p>
<p>Amazon’s <del>ad-infested</del> home screen filled with recommendations isn’t respectful of our time and eyeballs. Recommendations on the home screen don’t help us finish our book collection.</p>
<p>No, this screen, filled with pretty covers was designed to make us purchase <em>even more</em> books. Not to showcase books we might already own, nor to encourage us to continue reading our current reads.</p>
<p>To be clear: book discovery can be a good thing, but you know when I want to look for the next book? When I’m done with the current one.</p>
<p>I consider an e-reader a novelty device with a singular feature – it’s a device with little to no distractions, exclusively intended for reading. Kobo’s home screen is far better, in my estimation.</p>
<img src="/media/blog/20260319_124844_843913/source.png" class="kobo" alt="Here's what the home screen looks like on my Kobo Libra Color without any modifications. I have customized the tab bar below via NickelMenu, but more on that later. (I've also desaturated the screenshots to better represent what it actually looks like on the device.)">
<p>As you can see, Kobo does a better job. There’s some advertising for Kobo Plus and the store, but it’s less oppressive. Despite this positivity, Kobo’s homescreen could be better:</p>
<ul>
<li>If you are reading only one book, the operating system will <u>sometimes</u> display a couple of other books on the right of your current read. I’ve usually found that juggling multiple books at once isn’t that fun.</li>
<li>There’s a third column below <em>My Books</em> that sometimes displays useful information (“You’re read for a total of 50 hours!”) but usually advertises Kobo’s subscription service or store.</li>
</ul>
<p>I like owning books, so I tend to just straight up buy them from the store when I need them. Advertising to me isn’t going to make me change my mind. My complaints about Kobo’s home screen UI are simply gone if those elements were simply something you could turn off.</p>
<p>Before they introduced the current home screen many years ago, you could actually dismiss individual tiles from the old home screen, which was cool.</p>
<p>I wish they re-introduced this functionality, but… what if you could do that today?</p>
<h2>Patching it out</h2>
<p>Well… as it turns out, you can. Kobo devices run their own custom software, running on Linux with their own GUI based on Qt. It’s a fairly open system compared to the locked down proprietary stuff on Kindle.<sup id="fnref:jailbreak"><a class="footnote-ref" href="#fn:jailbreak" role="doc-noteref">3</a></sup></p>
<p>There are custom patches that you can apply that do this. All you need is a copy of the system software (which you will need to download the correct version for your device of) the patches, and <code>kobopatch</code>.</p>
<p>The latter is a command line tool that you can point at a particular patch so you can apply it to the system software, which you can install after patching it.</p>
<p>Here’s how this process usually went:</p>
<ul>
<li>Rakuten Kobo releases a new version of their software, so you update your device. Any existing custom patches you made are now gone.</li>
<li>You can usually find a <code>zip</code> with <a href="https://www.mobileread.com/forums/forumdisplay.php?f=247">patches on the MobileRead forums</a>. You can search for “Patches for Firmware” and then the version number of your firmware.</li>
<li>To do this, you must first note what operating system version you are using.</li>
<li>You can then download a copy of the software via <a href="https://pgaskin.net/KoboStuff/kobofirmware.html">the Kobo firmware page</a> kindly set up by Patrick Gaskin. This is also the author of <code>kobopatch</code> and NickelMenu, by the way.<sup id="fnref:pgaskin"><a class="footnote-ref" href="#fn:pgaskin" role="doc-noteref">4</a></sup></li>
<li>Once you have the files saved in the right location can then apply patches via <code>kobopatch</code>. For up-to-date instructions, use the <code>readme.txt</code> from the <code>zip</code> you got from MobileRead.</li>
<li>After applying the patch, you copy the <code>KoboRoot.tgz</code> file to the <code>.kobo</code> folder on your actual device via the USB connection.</li>
</ul>
<p>So, it’s not super complicated, but it’s a tad annoying to do, and I’m always a little bit paranoid that I’m going to download the wrong software files and brick my device.<sup id="fnref:bricked"><a class="footnote-ref" href="#fn:bricked" role="doc-noteref">5</a></sup></p>
<p>For me, I used to apply this patch in <code>kobopatch.yaml</code>, and only this one:</p>
<pre><code class="language-yaml">overrides:
  src/nickel.yaml:
    Remove footer (row3) and increase cover size on new home screen: no
    Remove footer (row3) on new home screen: yes # &lt;--- I changed this!
</code></pre>
<p>However, recently a new software update was released for my Kobo Libra Color, and the previously applied patch was no longer applied because it was overwritten by the new system libraries that came with the updated system software.</p>
<p>This is in stark contrast to my NickelMenu configuration, which persists when system updates are installed due to the nature of the injection mechanism behind NickelMenu. Also, there are a bunch of safety mechanisms built-in:</p>
<blockquote>
<p>A built-in failsafe will automatically remove NickelMenu if any fatal error occurs, and will never result in an unbootable Kobo. Non-fatal errors will appear as a menu item with more details.</p>
</blockquote>
<p>Super cool. For a more technical deep dive, I recommend checking out <a href="https://github.com/pgaskin/NickelHook">NickelHook</a>, which is a part of what makes NickelMenu work in this way.</p>
<p>This involved process I documented above is what things were like until recently.</p>
<h2>KoboPatch Web UI</h2>
<p>But then I thought: wouldn’t it be easier if I could just connect a device and have the CLI automatically recognize what kind of device it is?</p>
<p>I could probably automate downloading the system software… then, patching it on the fly is a trivial and automated process because <code>kobopatch</code> exists.</p>
<p>Maybe, if I used WebAssembly, I might even do all of this patching <em>in the browser</em>, so I wouldn’t have to download the patches every time? I could theoretically patch my Kobo from anywhere this way.</p>
<p>Okay, well, that doesn’t sound too bad. If I could just click a button on a web page and it’d generate a <code>KoboRoot.tgz</code> patch file for me, that would literally be the easiest way to set this up, barring having to copy that file to the actual device.</p>
<p>That reminded me: I also remembered that Google Chrome had an API that allows you to give it access to a folder on the filesystem, so if you point it at your Kobo, literally all of this could potentially be automated.</p>
<p>The whole process could become a simple wizard with three steps: identify device, apply patches and copy the files onto the device, all done by the browser with a few clicks.</p>
<p>At that point, all it takes is connecting the device via USB and safely ejecting it when done. The WASM bit was where I was the most concerned, but thankfully <code>kobopatch</code> was written in Go… which is <a href="https://go.dev/wiki/WebAssembly">good news</a>.</p>
<p>Woah. Technically, this could be doable! This could be <strong>very</strong> cool!</p>
<p>Now, to be clear: this was just an idea at this point. But I wanted to see what was possible, so I wrote a <a href="https://github.com/nicoverbruggen/kobopatch-webui/blob/d646a4b766d41ae98c5fd99e1d5cf6b3507427de/README.txt">short and simple text file</a> with what my technical requirements were, and got to work with my favorite LLM to see if I could get a prototype up and running and verify the actual viability of this project.</p>
<p>It didn’t take very long to get some promising results. These large language models sure are something, if you know how to properly instruct them and check things carefully.</p>
<p>Sure enough, after an hour or three of tinkering, I had a rough version up and running with the key WASM <code>kobopatch</code> conversion passing all of the tests.<sup id="fnref:llm"><a class="footnote-ref" href="#fn:llm" role="doc-noteref">6</a></sup></p>
<p>Wild.</p>
<p><img src="/media/blog/20260319_132620_978357/source.png" alt="This is the initial prototype I came up with. In the current version, tweaks that belong to the same group of patches are actually grouped together and can be disabled. In this prototype, that wasn’t possible, and there were quite a few janky things." /></p>
<p>So, case closed? I could probably close the book on this. This would be the easiest way ever to patch my Kobo devices with these patches.</p>
<p>I would just need to connect them via USB… and it all worked.</p>
<p>It was great. Until I had an even better idea.</p>
<h2>Forking NickelMenu</h2>
<p>The problem with applying the specific fixes I mentioned earlier, is that you need to re-apply these patches every time a new update comes out.</p>
<p>That’s annoying. A few other factors are also in play:</p>
<ul>
<li>How quickly Rakuten Kobo releases updates</li>
<li>How quickly it takes for patches to be verified</li>
<li>How difficult it is to patch your device</li>
<li>How many different devices you need to patch</li>
</ul>
<p>If I was away or on holiday and an automatic update were to install itself (as it does, sometimes, for minor updates) then my customizations for the homescreen would be gone. It’s not a super big issue, but it’d be unfortunate.</p>
<p>This is not an unrealistic situation – my Kobo Clara Color actually did this to me when the March 2026 update came out. I picked it up and the crowded home screen was back after an update had installed itself overnight after syncing.</p>
<p>Frankly, if this particular home screen tweak were to <em>persist</em> even after updates are installed, it’d be even better. This brings me to my better idea.</p>
<p>Here’s what you should know first.</p>
<p>I believe I mentioned NickelMenu earlier. It is actually a modification that <em>persists</em> after software updates, because of how it works. It patches onto the system partition a new file that never gets overwritten by Kobo’s software packages, but can uninstall itself if it deems itself incompatible.</p>
<p>It introduced an experimental feature in the last update that allows you to customize the navigation tab bar at the bottom of your device, even to the point of being able to hide specific tabs and renaming them.</p>
<p>In the NickelMenu configuration file in <code>.adds/nm/items</code>, you can do that like this:</p>
<pre><code class="language-yaml"># Menu button customization

# this line renames &quot;My Books&quot; to &quot;Books&quot;
experimental :menu_main_15505_1_label: Books
# this line hides the notebook
experimental :menu_main_15505_3_enabled: 0 
</code></pre>
<p>So, I wondered: what if I could also do something similar to the relevant widgets on the homescreen – as in, hide them? I knew that Nickel, the software running on Kobo devices is all Qt-based, so in theory it should be possible.</p>
<p>Now, I have very little experience writing C++, but there were plenty of examples in the existing code, so all it took was adding two sections to <code>src/nickelmenu.cc</code>.<sup id="fnref:cpp"><a class="footnote-ref" href="#fn:cpp" role="doc-noteref">7</a></sup></p>
<p>I’ve annotated the code to make it easier to understand. Okay, first, the registration of the hook and the necessary type information:</p>
<pre><code class="language-c++">// fake type alias for an unknown class so we can hook it;
// we treat it as a QWidget because we don't have the actual
// class definition (no source code access)
typedef QWidget HomePageView;

// function pointer to the original constructor
// filled in by the hook system below
void (*HomePageView_HomePageView)(HomePageView*, QWidget* parent);

static struct nh_hook NickelMenuHook[] = {
	// (other hooking omitted, reformatted for easier reading)
	// ...
    // home page widget hiding (15505+)
    // this is where the actual hook is registered
    {
    .sym = &quot;_ZN12HomePageViewC1EP7QWidget&quot;, // original constructor...
    .sym_new = &quot;_nm_homepageview_hook&quot;, // replacement function
    .lib = &quot;libnickel.so.1.0.0&quot;,  // in this library
    .out = nh_symoutptr(HomePageView_HomePageView), // we store the original constructor here
    .desc = &quot;home page widget hiding (15505+)&quot;, 
    .optional = true // skip hook if not found
    }, //libnickel 4.23.15505 * _ZN12HomePageViewC1EP7QWidget
    
    // additional note
    // the libnickel annotation w/ version is for the test workflow
    
    // ...
};
</code></pre>
<p>After that, it was a matter of adding the actual hook itself, which accomplishes what I’ve been meaning to get working – i.e. the hiding of certain elements on the homescreen. That’s <em>hiding</em>, not removal – as I didn’t want Nickel to crash.</p>
<pre><code class="language-c++">extern &quot;C&quot; __attribute__((visibility(&quot;default&quot;))) void _nm_homepageview_hook(HomePageView *_this, QWidget *parent) {
	// it's always good to log these kind of things
    NM_LOG(&quot;HomePageView::HomePageView(%p, %p)&quot;, _this, parent);
    
    // call original constructor before modifications!
    HomePageView_HomePageView(_this, parent);

	// identify which object names we will look for
    const char *hide_widgets[] = {&quot;row1col2&quot;, &quot;row3&quot;};

	// we'll loop over those potential widgets
    for (size_t i = 0; i &lt; sizeof(hide_widgets) / sizeof(hide_widgets[0]); i++) {
	    
	    // identify what key we need in the config file for this one
        char key[32];
        snprintf(key, sizeof(key), &quot;hide_home_%s_enabled&quot;, hide_widgets[i]);
        
        // read the configuration value from the config file
        const char *val = nm_global_config_experimental(key);
        
        // ensure configuration value is present and enabled
        if (!val || strcmp(val, &quot;1&quot;))
            continue;
            
        // attempt to find the widget based on Qt objectName
        QWidget *w = _this-&gt;findChild&lt;QWidget*&gt;(QString::fromLatin1(hide_widgets[i]));
        
        if (w)
	        // and if we can actually find it, hide it at runtime
            w-&gt;setVisible(false); // this is where the magic happens
        else
	        // or log that the widget could not be found
            NM_LOG(&quot;warning: could not find home page widget '%s' to hide (it may not exist on this firmware version)&quot;, hide_widgets[i]);
    }
}
</code></pre>
<p>Once again, an LLM came to the rescue when building this and I was able to come up with this – I triple checked it for errors, made sure I properly understood what was happening (and understood the safeguards) and then tried it on actual hardware.</p>
<p>Sure enough, my forked version of NickelMenu didn’t cause any issues that I could see, and worked like before, but I still had to actually enable the new feature for testing and reboot my device. The hook in particular kicks in when the home screen is first rendered, so it’s important to restart your Kobo if you want this to work.</p>
<p>Here’s how you enable those specific tweaks:</p>
<pre><code class="language-yaml"># Hides recommendations next to current read
experimental:hide_home_row1col2_enabled:1
# Hides info text (time read, store, kobo plus ads) below My Books
experimental:hide_home_row3_enabled:1
</code></pre>
<p>When I added the new configuration flags to my NickelMenu configuration file and rebooted, it worked just as expected – but without needing custom patches.<sup id="fnref:kindof"><a class="footnote-ref" href="#fn:kindof" role="doc-noteref">8</a></sup></p>
<img src="/media/blog/20260319_132201_872398/source.gif" class="kobo" alt="This GIF illustrates what the home screen looks like before and after the changes.">
<p>In the meantime, I’ve also opened a <a href="https://github.com/pgaskin/NickelMenu/pull/227">pull request</a> to NickelMenu with this change. Even if this code doesn’t make it in the actual project, it’s doing exactly what I needed it to do, so I’m very happy.</p>
<h2>Unifying both solutions</h2>
<p>Now, I had this pretty cool Web UI project, but what if it could also install NickelMenu? That would be awesome and the best of both worlds. It’d also be a much better and (not to mention) safer default option for anyone interested in tinkering with their device.</p>
<p>It would also allow me to install my customizations to all of my devices, and if I decide to update the customizations, all I need to do is plug in my device and go through the wizard, which only takes a few seconds.</p>
<p><img src="/media/blog/20260319_132500_726343/source.png" alt="This is what the website first looked like when you visited it using Chrome, when I first released the website. It looks a little more polished now." /></p>
<p>(I noted earlier in a footnote that I previously bricked my Libra 2 like two years ago when I attempted to downgrade my system software, and it <em>sucks</em> when you brick a device. You’d think I learned to stop tinkering with my Kobo devices after that happened, but no. All I’ve done is learn from my mistakes.)</p>
<p>I decided that I was going to integrate installing NickelMenu into the flow of the patcher. The idea was simple; at the start, you need to decide what you want to do:</p>
<ol>
<li><strong>Do you want to install NickelMenu with or without a preset?</strong> This works on most, if not all devices running a modern software version.</li>
<li><strong>Do you want to apply custom patches and install those to your device?</strong> This works only on a limited selection of devices via the web UI as of right now.<sup id="fnref:device_support"><a class="footnote-ref" href="#fn:device_support" role="doc-noteref">9</a></sup></li>
</ol>
<p>There’s definitely still some areas of improvement, and I plan on tackling those in the future, but I was able to get all of this working.</p>
<p><img src="/media/blog/20260319_132826_433879/source.png" alt="It’s possible to enable or disable various features that are part of the preset I’ve made available on GitHub. You can also choose to install NickelMenu without any configuration, or remove it." /></p>
<p>I should also note that my forked version of NickelMenu is downloaded in order to support the hiding of those home screen widgets, but should this change get merged, I will make sure to link to the original project again.</p>
<p>So, you might be thinking – I want to check that out and try it! I have some good news, because everything happens client-side, I was able to host a static site on my Coolify instance with a <code>nixpacks</code> configuration file.</p>
<p>I’ve hosted the project on a subdomain here:</p>
<div class="cta">
<a class="big-button" href="https://kp.nicoverbruggen.be" target="_blank">Try out KoboPatch Web UI</a>
</div>
<p>If you don’t have a device or don’t want to use Chrome, you can always choose to generate an archive with the patches and inspect it.</p>
<p>I hope if you’re a fellow tinkerer, you find it useful, too. (Even if it’s only to check out the generated zip files to see how it all works.)</p>
<p>In the meantime, the author of NickelMenu and <code>kobopatch</code> had some <a href="https://www.mobileread.com/forums/showpost.php?p=4574126&amp;postcount=15">good feedback</a> and provided me with some additional technical guidance in the MobileReads thread where I posted about this.</p>
<p>I hope to be able to further improve this project in the future. There’s definitely a few more cool things that would improve the general user experience and there’s still quite a lot of automation potential.</p>
<p>But for now, I’m going to take a bit of a break… and finish that book that I’ve been reading.</p>
<h2>Updates</h2>
<p>After initially writing this blog post, I’ve made some additions. You can find these below.</p>
<p>First off, if you want to experience this blog post in video form, I made a few helpful videos. First, the story of this blog post is partially explained in my first video:</p>
<iframe width="100%" height="400" src="https://www.youtube-nocookie.com/embed/lNtg_GfCups" title="YouTube video player" frameborder="0" allow="clipboard-write; encrypted-media; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<p>I also recorded a more recent video (a while after initially publishing this blog post) that you can find here:</p>
<iframe width="100%" height="400" src="https://www.youtube-nocookie.com/embed/5scZ-c43RVg" title="YouTube video player" frameborder="0" allow="clipboard-write; encrypted-media; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<p class="label section">Update</p>
<p class="label_aside">May 16, 2026</p>
<p>I have been updating the tool over the last few weeks to make it more easy to use, and I’ve fixed a bug that caused books to vanish. I’d like to apologize for that one, as it caused some unintentional data loss.</p>
<p>Initial issues like these due to edge case bugs or oversights are why I included a disclaimer to backup your sideloaded content, which I still recommend doing despite the fact that at this point KoboPatch Web UI is pretty battle-tested.</p>
<p>I’ve also updated my fork of NickelMenu so it’s a little more targeted when it comes to hiding elements, and it’s now also possible to hide the content next to “My Books” on the home screen.</p>
<p>(I should note: I previously mentioned this in a footnote. You can find the updated <a href="https://github.com/nicoverbruggen/NickelMenu">fork on GitHub</a> with this new change. For historical purposes, I’ve not amended the source code of my changes here in this blog post.)</p>
<p>I hope you continue to find KoboPatch Web UI useful!</p>
<p class="label section">Update</p>
<p class="label_aside">May 28, 2026</p>
<p>In the meantime, I’ve also recorded a <a href="https://www.youtube.com/watch?v=kHLrWklKxzU">a video</a> which acts as a demo where I apply the tweaks and install KOReader on my Kobo Clara BW. I’ve attached it inline to this blog post, above.</p>
<p>This is a more recent video than my <a href="https://www.youtube.com/watch?v=lNtg_GfCups">original demo</a> which also includes information about why I made this project. Both are worth a watch, but if you want to see how to quickly set things up, the video above is probably sufficient.</p>
<p class="label section">Update</p>
<p class="label_aside">June 14, 2026</p>
<p>I’ve pushed out a substantial update to KoboPatch Web UI, which will probably be the last significant one for a while. It adds support for NickelClock as an optional add-on, and gives you various new customization options.</p>
<p>Those who like custom patching will be happy to learn that you can now easily search for patches and edit them as needed, too. This was part of the initial feedback I got from folks on the MobileRead forums, and that’s now integrated.</p>
<p>To explain what has changed, I also recorded a 10-minute video:</p>
<iframe width="100%" height="400" src="https://www.youtube-nocookie.com/embed/Qz_xZDl8S68?si=0WPHTCzbfhlBGnqY" title="YouTube video player" frameborder="0" allow="clipboard-write; encrypted-media; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<p>Another thing I’ve done: in the event that my pull request doesn’t make it into NickelMenu, I’ve now created a standalone mod that does the same thing.</p>
<p>I’ve called mine <a href="https://github.com/nicoverbruggen/NickelHome">NickelHome</a>, and it allows you to apply the same home screen customizations that are baked into my NickelMenu fork, but without needing NickelMenu:</p>
<pre><code># Hide various home screen widgets

hide_home_row1col2_enabled:1
hide_home_row2col2_enabled:1
hide_home_row3_enabled:1
</code></pre>
<p>This <em>may</em> eventually get integrated into KoboPatch Web UI, which means I will be able to use the stock version of NickelMenu on the website, which should be easier to maintain and cause less issues and maintenance burden for the original author.</p>
<p>(The website using NickelHome isn’t the case right now, so keep that in mind. But it is something I am considering!)</p>
<p>So far, the website has been used a lot more than I’ve expected. I’ve had over 1.5k unique users complete a flow (that’s install or patch) so far, according to my anonymous analytics.</p>
<p>I’m confident that folks like knowing it’s easy to uninstall, because about 15% of users actually do that, too. Yay for easy and safe experimentation!</p>
<style>
figure, picture {
width: 100% !important;
display: inline-block !important;
text-align: center !important;
}

.kobo {
width: 502px;
margin: auto;
border: 1px solid #888;
filter: saturate(0.45) contrast(0.85) brightness(1.1) sepia(0.1);
image-rendering: auto;
}

.cta {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}

.post .content a.big-button {
text-decoration: none;
margin: auto;
padding: 7px 15px;
font-weight: bold;
background-color: var(--primary-color);
color: #FFF;
border: none;
border-radius: 15px;
margin: 20px 0px;
}
</style>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:georgia" role="doc-endnote"><p>On the Kobo Clara 2E, which used Georgia as the system font, the height of these text blocks was more or less fixed at two lines. But with the new system font, Rakuten Serif, this is no longer the case on the Kobo Clara 2E, which has a display with the same screen resolution. On this new device, the layout of the homescreen tiles will now be shifted because the text now needs 3 lines due to different font metrics (Rakuten Serif is slightly wider) which was likely not accounted for. The Activity screen is similarly broken for long book titles, by the way.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:georgia" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:amazon" role="doc-endnote"><p>I’m also not that big a fan of Amazon for other reasons, but that’s not relevant to this post. My dislike of big tech companies isn’t relevant to the broader point, and Amazon is probably less problematic than others.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:amazon" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:jailbreak" role="doc-endnote"><p>You might be familiar with jailbreaking. Because Kindle is a closed off device, hackers try to make it so you can put your own software on there. Amazon typically tends to fix the exploits that allow this to happen.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:jailbreak" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:pgaskin" role="doc-endnote"><p>What a legend! I’m positive that without Patrick’s contributions, there wouldn’t be such an active modding community for this platform. I’m grateful that all of this stuff was open sourced. It’s also really interesting to take a technical look at how all of this is accomplished, so I highly recommend reading up on how this is all accomplished and checking out the source code.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:pgaskin" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:bricked" role="doc-endnote"><p>Well, maybe it isn’t paranoia, because I actually bricked a Kobo Libra 2 once. The crucial mistake I had made was that I’d installed the firmware files of an older revision on a newer Libra 2 revision when I attempted to downgrade the software. I had no idea it was a newer revision at the time, but trust me: I know better now. I double – no, triple check these kind of things now.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:bricked" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:llm" role="doc-endnote"><p>While the technical “testing” of this initial version took only three hours, it would take three days to properly polish something that I’d call a nice 1.0 version with a decent user experience.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:llm" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:cpp" role="doc-endnote"><p>The closest I’ve come to writing C++ is when I learned some C, which was a prerequisite for Objective-C at the start of my career in 2014 – and it isn’t like Objective-C is very relevant these days… If it wasn’t for the assistance of LLMs, this would have taken a lot of tinkering for someone unfamiliar with the language, and frankly, I might have given up.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:cpp" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:kindof" role="doc-endnote"><p>To be entirely correct: well, two out of the three toggles I added did. I had to remove one (<code>row2col2</code>) because it also affected the “My Books” section, as it made individual books in the “three books next to each other” view disappear. It didn’t even properly remove the last viewed Collection – which was sort of the point – so this was clearly not the right way to identify and hide that particular widget on the homepage. (In the future, I suppose I could still try to enumerate each of the elements and see if there’s another one I can potentially disable, but that’s something for another time.)&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:kindof" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:device_support" role="doc-endnote"><p>I only have one fixed version that is supported in the web UI, and that version is only installable on Kobo Libra Colour, Kobo Clara Colour, and Kobo Clara BW. This may change in the future, but for now I’ve decided to limit support to those devices.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:device_support" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Fri, 20 Mar 2026 12:59:00 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[Building Readerly]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/creating-readerly" />
        <id>https://nicoverbruggen.be/269</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p class="headline">When I got back into reading in 2022 with my first steps into the Cosmere, I did not imagine that I would become a reading fiend and make my way through many books as the years have gone by.</p>
<p>I also ended up owning a bit of an e-reader collection. I’ve owned Kobo, Kindle and Boox devices over the years. My Kindle ownership was brief: after a couple of months, I sold the device but there was one thing that I thought was excellent on Amazon’s device – and that’s the fonts that shipped with the device.</p>
<p>I’ve written about the topic before, but Rakuten Kobo’s default fonts are not as good as they used to be. I understand that the licensing costs may have been a problem. Now that they ship a different selection of fonts with their devices, I think it’s fair to say their default type options are frankly not as good as Kindle’s, which ships with the likes of Caecilia, Palatino and most importantly, the excellent <a href="https://en.wikipedia.org/wiki/Bookerly">Bookerly</a>.</p>
<img class="type" src="/media/blog/20260307_211518_441337/source.svg" alt="Bookerly is &copy; Dalton Maag Ltd., and licensed to Amazon. This image is for illustrative purposes only. This particular typeface sample has been sourced via Wikipedia.">
<p>I am aware that there’s a bunch of people who just download whatever fonts they want, but that’s not something you are legally supposed to do – <a href="https://fonts.google.com/knowledge/glossary/licensing">licensing</a> restrictions apply to commercial fonts (and <a href="https://fonts.google.com/knowledge/glossary/typeface">typefaces</a>). Bookerly is a serif typeface designed by Dalton Maag, and it’s exclusively licensed to Amazon.</p>
<p>A few years ago, Rakuten got a bunch of fonts made for their brand. It’s notable that these were also created in <a href="https://global.rakuten.com/corp/news/update/2020/0702_01.html">collaboration with Dalton Maag</a>. As of the release of the Kobo Libra Color, Rakuten Serif is now the default font (superseding <a href="https://en.wikipedia.org/wiki/Georgia_(typeface)">Georgia</a>) for the digital reading experience.</p>
<p>This new default, Rakuten Serif sadly just isn’t that great if you prefer your fonts a little thicker – which is generally easier on the eyes on e-ink displays. To be fair, this is partially an issue of taste. I can see that some people really find it very crisp, and it’s definitely quite legible. However, as someone without perfect vision, I actually rather prefer a little bit more weight on my fonts.<sup id="fnref:slab"><a class="footnote-ref" href="#fn:slab" role="doc-noteref">1</a></sup></p>
<p>The most common open source license that does allow for modification, sharing, etc. is the <a href="https://openfontlicense.org/">Open Font License</a>. Sadly, as I noted before, there’s no real similar alternative to Bookerly that currently exists that you can legally acquire and put on your e-reader.</p>
<p>I’ve maintained the <a href="https://github.com/nicoverbruggen/ebook-fonts">ebook-fonts</a> repository for quite a while now, and while I was preparing to update my curated selection for the upcoming “v3” release, I wanted to give <a href="https://fonts.google.com/specimen/Newsreader">Newsreader</a> another go and see if I couldn’t put it in my <em>core</em> collection.</p>
<p>Unfortunately, Newsreader is quite beautiful but I found its stroke thickness less than ideal. The font is, however, interesting: when you look at the variable font features, you can modify two aspects of the font: the <a href="https://fonts.google.com/knowledge/glossary/weight">weight</a> and the <a href="https://fonts.google.com/knowledge/glossary/optical_size_axis">optical scale axis</a>.</p>
<blockquote>
<p>Optical sizes in a variable font are different versions of a <a href="https://fonts.google.com/knowledge/glossary/typeface">typeface</a> optimized for use at singular specific sizes, such as 14 pt or 144 pt. Small (or <a href="https://fonts.google.com/knowledge/glossary/body">body</a>) optical sizes tend to have less <a href="https://fonts.google.com/knowledge/glossary/stroke">stroke</a> <a href="https://fonts.google.com/knowledge/glossary/contrast">contrast</a>, more open and wider spacing, and a taller <a href="https://fonts.google.com/knowledge/glossary/x_height">x-height</a> than those of their large (or <a href="https://fonts.google.com/knowledge/glossary/display">display</a>) counterparts.</p>
</blockquote>
<p>If you download Newsreader via Google Web Fonts, you will get the variable font, as well as some static size combinations.</p>
<p>When I looked at the different styles, I noticed that the 9pt optical version looked pretty good. It actually reminded me of Bookerly, but it was a little wider. That led me to first create “NV NinePoint”, a renamed version of Newsreader 9pt. (I know, a very creative choice.)<sup id="fnref:rename"><a class="footnote-ref" href="#fn:rename" role="doc-noteref">2</a></sup></p>
<p>I tried out NinePoint for a while, but I found it eerily reminiscent of Bookerly, in some aspects. So I wondered: how close can I get to the look and feel of Bookerly by altering this basis? I’d start from the basis of Newsreader and then do some transformations and see what looks optically the most interesting or readable.</p>
<p>Since I was going to optimize for e-readers, I decided that I’d bump the weight of the font by a little, too. Because it would be a very time consuming job to make these changes manually via FontForge’s UI, I decided to see if I couldn’t automate this process. Some tinkering was needed, but with the assistance of some LLMs, I was able to accomplish the task.</p>
<p>Then it was up to me to decide what font weights to use, and what transformations to apply. In order to get closer to the visual appeal of Bookerly, I wanted smaller uppercased letters, so those have been made a little shorter (or, well, the others have been made taller, to be more precise) so that the optical size is a little different.</p>
<p>This unfortunately means that certain lowercase characters may end up taller than certain tall capitals. While a little bit typographically dubious, I do also think it looks kind of interesting, so I’ve kept that modification in.</p>
<p>In order to best illustrate the difference between the original Newsreader and the altered version, I think this image properly illustrates what’s changed.</p>
<img class="bordered" src="/media/blog/20260307_220326_807459/source.svg" alt="As you can see, all of the characters in Readerly are a bit condensed and a little taller, too, compared to Newsreader 9pt. I made sure to check if the weight looks good on e-readers.">
<p>I’ve made it so that the line spacing on Readerly is fairly tight, so that you can customize it. I set up various variables that are configurable in the Python script should you want to make your own modifications. The defaults are as follows:</p>
<pre><code class="language-py">VARIANT_STYLES = [
    # (style_suffix, source_vf, wght, opsz)
    # opsz=9 is intentionally small to tighten letterforms
    (&quot;Regular&quot;,    REGULAR_VF, 450, 9),
    (&quot;Bold&quot;,       REGULAR_VF, 650, 9),
    (&quot;Italic&quot;,     ITALIC_VF,  450, 9),
    (&quot;BoldItalic&quot;, ITALIC_VF,  650, 9),
]

# Scale lowercase glyphs vertically (and slightly widen).
SCALE_LOWER_X = 1.03
SCALE_LOWER_Y = 1.08

# Condense all glyphs horizontally.
CONDENSE_X = 0.95

# Step 3: Vertical metrics + line spacing (relative to UPM)
# - LINE_HEIGHT drives OS/2 Typo metrics (default line spacing)
# - SELECTION_HEIGHT drives Win/hhea metrics (selection box + clipping)
# - ASCENDER_RATIO splits the total height between ascender/descender
LINE_HEIGHT = 1.0
SELECTION_HEIGHT = 1.3
ASCENDER_RATIO = 0.8

# Step 3: Naming and style metadata (used by the rename step)
STYLE_MAP = {
    &quot;Regular&quot;:    (&quot;Regular&quot;,     &quot;Book&quot;, 400),
    &quot;Bold&quot;:       (&quot;Bold&quot;,        &quot;Bold&quot;, 700),
    &quot;Italic&quot;:     (&quot;Italic&quot;,      &quot;Book&quot;, 400),
    &quot;BoldItalic&quot;: (&quot;Bold Italic&quot;, &quot;Bold&quot;, 700),
}
</code></pre>
<p>This was done because I was constantly customizing the scaling and font weights to try various different configurations. It took me a bit to get something that I liked. It was important that it didn’t look like the font was just distorted, which is always a risk when you apply this kind of scaling to glyphs without manual modifications.</p>
<p>I then proceeded to test the various iterations of the font on my devices. As an example, here’s what Readerly looks like on a Kobo device:</p>
<img class="kobo" src="/media/blog/20260314_221101_167389/source.png" alt="Readerly on a Kobo Libra Color, with the first page of Warbreaker displayed.">
<p>I bumped into a couple of issues, at first. Two in particular are worth documenting.</p>
<ol>
<li>
<p>First off, I noticed that certain glyphs did not render correctly on Kobo devices, and this is most likely due to the nature of the variable font and how the outlines were originally designed. Thankfully, simplifying the outlines is generally the fix for this particular issue.</p>
</li>
<li>
<p>I also noticed that doing this caused some glyphs to be exported incorrectly. In particular, on macOS some glyphs (only a handful) would not render at all.<sup id="fnref:clean_contours"><a class="footnote-ref" href="#fn:clean_contours" role="doc-noteref">3</a></sup></p>
</li>
</ol>
<p>It turns out that I needed to do a little bit of cleanup after outline simplification. I had to ask an LLM to help me out with that, and it was able to identify so-called “zero-area contours”, which I was able to remove as a final step of the build script.</p>
<h3>Tweaks and modifications</h3>
<p>Anyway, so here’s the key modifications in a list:</p>
<ul>
<li>The regular (and italic) is based on the 450 weight, and 9 pt optical size.</li>
<li>The bold (and bold italic) is based on the 650 weight, and 9 pt optical size.</li>
<li>Lowercase glyphs are scaled to be 3% wider and 8% taller.</li>
<li>All glyphs are also condensed by 5% (so lowercase glyphs are 2% condensed).</li>
<li>Vertical metrics have been adjusted for a tighter line height.</li>
<li>Outlines have been simplified and zero-area contours after simplification have been removed.</li>
<li>The font has been renamed to Readerly, copyright information has been updated and the version number is distinct from Newsreader.</li>
</ul>
<p>With this, I hope to have provided a potential decent alternative to Bookerly for most extended Latin scripts. This means that the font can be used for most Western European languages.</p>
<p>I should note that Newsreader does not come with support for Arabic, Armenian, Cyrilic, Greek, Greek, Hebrew or Hindi alphabets. As such, Readerly does not either.</p>
<p>If you need broad support for such other alphabets, you should look into other options, such as <a href="https://fonts.google.com/noto">Noto</a> or <a href="https://software.sil.org/charis/">Charis</a>.</p>
<img class="type" src="/media/blog/20260307_211538_323672/source.svg" alt="I tried my best to create a similar image for Readerly, and this is what it looks like.">
<p class="label section">Update</p>
<p class="label_aside">March 14, 2026</p>
<p>Readerly has received an update today that adds font hinting via <code>ttfautohint</code>, which makes it look significantly better on Kobo devices.</p>
<p>Without explicit font hinting, the font unfortunately didn’t look correct, with some glyphs seemingly “jumping up” due to how the renderer (likely FreeType) handles unhinted fonts.</p>
<p>Here’s a little demonstration video (without audio) that demonstrates the difference as I toggle between two layers in Affinity Photo.</p>
<p><video playsinline class="bordered" width="100%" controls loop src="/media/blog/20260314_195801_959057/source.webm"></video></p>
<p>Hopefully, you can see that initially, when the video starts, some of the glyphs seem to be a little off. In the initial picture, you can see that the <code>u</code> glyph seems to be moved 1 pixel up from the baseline, which gives the impression that the <code>u</code> and <code>a</code> characters aren’t correctly visually aligned.</p>
<p>This is especially noticeable on the actual e-readers – so the updated hinting does a better job at ensuring that all lowercase characters are visually identical in terms of height. Happy reading!</p>
<p>I’ve also set up an automatic pipeline for GitHub Actions that automatically generates a KF version of Readerly, which includes old-style kern pairs which is <a href="https://nicoverbruggen.be/blog/kerning-rabbit-hole">optimal for Kobo devices</a>, this is accomplished with my <a href="https://github.com/nicoverbruggen/kobo-font-fix">Kobo Font Fix</a> script.</p>
<p class="label section">Update</p>
<p class="label_aside">April 3, 2026</p>
<p>I have just released the latest collection of <code>ebook-fonts</code> and after lots of tweaking, version 1.5 of Readerly is included.</p>
<p>It’s pretty much as optimized for Kobo devices as it can be. For the absolute best reading experience, KOReader is recommended. You can also install it using <a href="https://kp.nicoverbruggen.be">KoboPatch WebUI</a>.</p>
<p>As an aside, I’m currently working on using machine learning to automatically kern a slightly thicker version of Charter (more on that in a future blog post) and I also released <a href="https://github.com/nicoverbruggen/sourcerer">Sourcerer</a>, a slightly thicker version of Source Serif for e-readers.</p>
<h3>Download</h3>
<p>If you would like to download the font so you can use it on your e-reader, you have a few options.</p>
<ul>
<li>Download the latest release for <a href="https://github.com/nicoverbruggen/readerly/releases/latest/download/KF_Readerly.zip">Kobo devices</a> directly.</li>
<li>Download the latest release for <a href="https://github.com/nicoverbruggen/readerly/releases/latest/download/Readerly.zip">other e-readers</a> directly.</li>
<li>You also can find my curated collection <a href="https://github.com/nicoverbruggen/ebook-fonts">here</a>.</li>
<li>Alternatively, you can <a href="https://github.com/nicoverbruggen/readerly">clone the Readerly repository</a> and run <code>build.py</code> yourself. This lets you build your own tweaked version to your personal taste if you’d like to adjust some of the knobs in the script.</li>
</ul>
<h3>Installation</h3>
<p>To install the font, please follow the instructions that apply to your e-reader or computer. For most devices, this means copying the TrueType font files, so make sure to unzip the archive first.</p>
<p>On Kobo devices, make sure to install the KF font files in the <code>fonts</code> folder on your <code>KOBOeReader</code> volume.</p>
<p>If you had a previous version of Readerly installed, also restart your device for the new version to be loaded.</p>
<h3>A few notes</h3>
<p>I do not wish to take a lot of credit for this. The original type designers from <a href="https://productiontype.com/">Production Type</a> clearly put a lot of effort into Newsreader, and they deserve all of the cheers and appreciation for their hard work.</p>
<p>I just made a script that creates a modified font, and while I did spend a couple of hours getting things just right, I didn’t even do much manual work in FontForge this time (only exploratory work, which I then automated).</p>
<p>In order to comply with the OFL, I also wanted to make sure that I renamed the font from Newsreader to Readerly, since the former is a reserved name.</p>
<style>
figure, picture {
width: 100% !important;
display: inline-block !important;
text-align: center !important;
}

.kobo {
width: 500px;
margin: auto;
border: 1px solid #888;
}

.type {
width: 500px;
margin: auto;
border: 1px solid #888;
}

.bordered {
border: 1px solid #888;
}
</style>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:slab" role="doc-endnote"><p>On older devices, Caecilia was also a great option. Unfortunately, that’s no longer an option. The good news is that for the most part, Bitter is an excellent slab serif alternative. Yes, I know that it’s possible to use the weight slider on Kobo devices to boost the weight of a font. In fact, it’s something I often do – but it’s also something that sometimes doesn’t work well with custom fonts, which is something I remedied with Readerly, by the way.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:slab" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:rename" role="doc-endnote"><p>I actually had to rename the font to that in order to make it work on Kobo devices, because fonts cannot contain a number or they won’t work reliably on the device, I’ve found. Oops. Pretty annoying, actually, but not a problem if you can rename the font.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:rename" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:clean_contours" role="doc-endnote"><p>I should note that if you manually export the font via FontForge, you will notice some issues on macOS – you need to run the <code>clean_ttf_degenerate_contours</code> function to fix this.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:clean_contours" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sun, 08 Mar 2026 12:00:00 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[The End of Black Library]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/black-library-app" />
        <id>https://nicoverbruggen.be/264</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>A couple of years ago, in 2018, after becoming a fan of <em>Total War: WARHAMMER</em>, I started reading some of the Warhammer Fantasy books. I started with the Tyrion and Teclis trilogy, after getting really intrigued by the lore in WARHAMMER II.</p>
<p>At the time, these books were available for purchase via the <a href="https://www.blacklibrary.com/">Black Library</a>, which is run by Games Workshop. It allowed DRM-free <code>.epub</code> purchases of their books via the website. Alas, soon, I will no longer be doing so.</p>
<p>You see, over the years, I’ve purchased all of my Warhammer related reads here, simply because I thought it was the best place to get these books – also, you know, straight from the publisher, where books were easy to find.</p>
<p>A few days ago, I received an email where they proudly announce their new app, which means that they’ll be sunsetting their website:</p>
<blockquote>
<p>BlackLibrary.com is moving to a new Black Library App, where you can buy, read and listen to our latest range of eBooks, audiobooks, eShorts (Shorts) and Audiodramas right at your fingertips. You will be able to browse and download past purchases on BlackLibrary.com until 10:00am (BST) on 31st August 2026, however you will not be able to place any new orders from 10:00am (GMT) on 18th March 2026.</p>
</blockquote>
<p>Unfortunately, this means that Black Library is doing away with their DRM-free <code>.epub</code> downloads, which was one of the best selling points of the platform.</p>
<p>Now, they’ll not only restrict their books to the app, but also migrate my purchases over to their new app, which means that I’m forced to rely on the archive I’ve made of my own purchases. Thankfully, I can read my purchased copies anywhere, simply because they were DRM-free when I purchased them.<sup id="fnref:drm"><a class="footnote-ref" href="#fn:drm" role="doc-noteref">1</a></sup></p>
<p>If I had not done that, later this year, my purchases will only be available on my smartphone, which is most assuredly <u>not</u> where I read my books.</p>
<p>Their FAQ states:</p>
<blockquote>
<p><strong>Question: Can I upload an ebook I’ve bought to the ereader I already have? (Kindle, Kobo, Apple iPad etc.)</strong><br/><br/>
Answer: No, but the great thing about the new Black Library App is that it provides everything you need for exploring, reading and listening, all within the App. You can still purchase Black Library eBooks and Audiobooks through alternative vendors to use with your preferred eReader.</p>
</blockquote>
<p>I actually have a Boox device that runs KOReader where I’ve sideloaded all of my DRM-free books. Looks like that’s no longer an option if I’d like to purchase any books via Black Library in the future.</p>
<p>(One of the wonderful benefits of not being locked into a particular platform is that you can move your purchases to whatever reader you prefer; whether that’s a smartphone, tablet, e-reader or something else.)</p>
<p>Looks like I’ll be purchasing my Warhammer books via Kobo’s digital storefront from now on… A shame, really.</p>
<p>As someone who’s pretty opinionated about digital reading to the point that he’s authored an open source <a href="/fonts">font collection for e-readers</a>, I must say I am very disappointed. Besides, what serious readers want to exclusively read their digital books on their smartphone?</p>
<p>Black Library was a great example of an online storefront selling digital books without bullshit. I guess enshittification comes for everyone these days.<sup id="fnref:enshit"><a class="footnote-ref" href="#fn:enshit" role="doc-noteref">2</a></sup></p>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:drm" role="doc-endnote"><p>When platforms die or change in ways that we do not like, this is where DRM-free content shines. I can just keep my content and the owner of the platform has nothing to say about what I do with my own, legally purchased copy of the book. Of course, copyright infringement remains illegal – which means not sharing the book, which is obvious. When you purchase a digital book you get a digital copy for your personal pleasure only.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:drm" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:enshit" role="doc-endnote"><p>Enshittification is an informal word used to criticize the degradation in the quality and experience of online platforms over time. Coined by Cory Doctorow: “Here is how platforms die: first, they are good to their users; then they abuse their users to make things better for their business customers; finally, they abuse those business customers to claw back all the value for themselves. Then, they die. I call this enshittification, and it is a seemingly inevitable consequence arising from the combination of the ease of changing how a platform allocates value, combined with the nature of a “two-sided market”, where a platform sits between buyers and sellers, hold each hostage to the other, raking off an ever-larger share of the value that passes between them.”&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:enshit" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sun, 22 Feb 2026 10:00:00 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[Current Thoughts on LLMs]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/thoughts-on-llms-2026-q1" />
        <id>https://nicoverbruggen.be/263</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>Over the last little while, I’ve been wondering: what even is the point of remaining a software developer if <em>[insert large language model here]</em> can do a decent job in a profession that I’ve been doing for the last decade? Frankly, am I cooked?</p>
<blockquote>
<p><strong>Note</strong>: While many people are using “AI” to refer to this new technology, I think it’s preferable to use a more concrete description of the exact technology in use; a large language model (“model”), an agentic coding workflow or tool like <a href="https://code.claude.com/docs/en/overview">Claude Code</a> or <a href="https://opencode.ai/">OpenCode</a>. When I use “AI”, I will put it in quotes, like here.</p>
</blockquote>
<h3>Long-Term Viability</h3>
<p>What makes software engineering a viable long-term career if this is the pace at which various agentic solutions are increasingly capable of doing my job?</p>
<p>While my input is currently necessary, will that still be the case in the future? It’s a concern that should be weighed seriously, and a bleak outlook worth examining.</p>
<p>I find it a difficult prospect, and this has turned into a bit of an existential question. Software development as I’ve done it for the majority of my career is significantly impacted by the rise of this new technology.</p>
<p>I understand that in the future, we will probably still need developers. Will we need as many as we used to, though? More importantly, am I a good enough developer to actually make the cut and not become totally irrelevant?</p>
<p>Also, what’s even the point if I don’t enjoy the delegating and reviewing that these new agentic coding systems require? If I want to be able to deliver a product that I actually believe in and fully understand, I usually need to work on things incrementally.</p>
<p>There are different ways to think about this, but often this involves grasping the domain knowledge, and using your own experience to create or improve a system tailored to the domain knowledge in question. It’s creating something of value, using my personal expertise, creativity, instinct, and such.</p>
<p>But these things are now increasingly less valuable, with LLMs being able to handle part of the heavy lifting here. So, what am I bringing to the table, again?</p>
<p>There is a trade-off, however. It’s an easy one to understand, but we should acknowledge it and be honest about it: the quality of the generated output will, in certain cases, not be good enough compared to when a person would actually do the work themselves, and this matters for foundational work.<sup id="fnref:foundations"><a class="footnote-ref" href="#fn:foundations" role="doc-noteref">1</a></sup></p>
<p>Some of this, what I would call “proper work”, is now heavily discouraged. Understandably so, when you can just tell the system to get to work and get things done in an instant.</p>
<p>This is possible because these days, the output is usually “good enough”, even without deep understanding or a comprehensive amount of context on the LLM’s side.</p>
<h3>Favouring Builders</h3>
<p>Maybe I’m just very peculiar about understanding what I am working on, and feeling a sort of disconnect here. Maybe I’m still adapting to the new way things can be done. To me, though, it seems as though there are two kinds of people who are now responding to this new technology.</p>
<p>First, you have people who enjoy programming on a language level, tinkerers. They are slow, but deep workers.</p>
<p>They enjoy a particular syntax, and they enjoy writing code and thinking through problems while coming up with solutions in an elegant and reusable way. Their ability to fully grasp how everything works (in detail) makes them truly remarkable and the people best suited to making certain technical decisions.</p>
<p>These people are not happy that the thing they enjoy the most about programming is now quickly becoming obsolete.</p>
<p>On the other side of the spectrum you have people who primarily enjoy building things, in a more broad sense.</p>
<p>Those people, comparatively, seem to fully embrace this new technology and are finding themselves no longer burdened by the chains of “bothersome” details. They’re fine giving up some control because the productivity boost is making the thing they enjoy the most more enjoyable. They can ship things faster than ever, and they absolutely love it. They feel a lot more productive because they are no longer wasting energy on certain decisions that come up when programming.<sup id="fnref:decision"><a class="footnote-ref" href="#fn:decision" role="doc-noteref">2</a></sup></p>
<p>Both sides are, unequivocally, right to feel the way they feel about things. However, one side will certainly find their careers threatened, and the other side will be able to truly capitalize on current developments.</p>
<h3>Vibe Coding Evolved</h3>
<p>Last year, I wrote about the implications of having these systems run the show in my blog post on <a href="https://nicoverbruggen.be/blog/vibe-coding">vibe coding</a>, as we become increasingly reliant on a service we rent, which could lead to us becoming lazy and certainly dependent on a third party if we want certain work to be done:</p>
<blockquote>
<p>Our bodies will adapt if we choose a lazy, sedentary, non-thinking lifestyle where no thought is needed at all. […] This way we are closer than ever to becoming slaves to our machines, if we aren’t already there.</p>
</blockquote>
<p>Something I didn’t note but we should also consider is that there’s only a handful of big companies with the GPU resources to keep evolving and serving this technology to the broader public. I’ll discuss this later in this post.</p>
<p>In last year’s blog post, I also made the point that doing the hard work and thinking for ourselves is valuable:</p>
<blockquote>
<p>Like going to the gym, I think there’s value in doing the hard work by actually thinking through things and training our brain, in order to remain sharp.</p>
</blockquote>
<p>I remain convinced this is still the case, and I will explain why. All of this despite the fact that the models have improved significantly.</p>
<h3>Time is Money</h3>
<p>Let’s be real. Like with the industrial revolution, all of this is (mostly) about time savings. Because time is money, baby.</p>
<p>Another fair criticism I can levy at this particular technology is that we are now creating more low-quality things (crap?) that we perhaps otherwise would not have, simply because it’s easy. We have already got new vocabulary to talk about this kind of thing. <a href="https://en.wikipedia.org/wiki/Clanker">Clanker</a> and <a href="https://en.wikipedia.org/wiki/AI_slop">AI slop</a> have entered the vernacular as a result.</p>
<p>Even Microsoft, after shipping a bunch of horrible updates to Windows, is increasingly referred to as “Microslop” with their push for Copilot and AI technologies. Copilot is damn near everywhere, and it isn’t what everyone wants deeply integrated into their operating system.<sup id="fnref:microslop"><a class="footnote-ref" href="#fn:microslop" role="doc-noteref">3</a></sup></p>
<p>Either way, there’s a strong push from corporations who would love nothing more than to drive the labour cost down for programmers and software development. It’s working. They’re winning, too.</p>
<p>We are becoming increasingly reliant on this new agentic technology that we cannot reliably run locally due to context window limitations.</p>
<h3>Iteration Speed</h3>
<p>One of the most frustrating trends over the last decade that I’ve noticed is that the speed at which things new things are being invented is steadily increasing. There’s constantly new stuff to get informed about, because new technology keeps getting invented. In a way, LLMs and agentic coding are being touted as an antidote to this problem.</p>
<p>While that may be the case (it is easier to keep up with a tool that can help you keep up), I know LLMs are also accelerating the “iterative development speed” in general substantially, further increasing the pressure on software developers to build things faster than ever, without thinking about what it is exactly that they are building.</p>
<p>There are two problems with this:</p>
<ol>
<li>The speed at which we work now increases, but our work hours remain the same.</li>
<li>We are now dependent on LLMs in order to “keep up”.</li>
</ol>
<p>Here’s another thought.</p>
<p>If you’ve ever learned anything about scams, you may have found out that scammers who want to misdirect generally love putting time pressure on individuals. Why? Putting time pressure on people forces them to act without thought.</p>
<p>I think we can take that school of thought and translate it to the current vibe I’m getting from the industry, and point out that: the faster the iteration time becomes, the less likely someone is to ask: “hey, should we even be building this, at all?” or, alternatively, “is this an ethical thing to do?”</p>
<h3>The Good Part</h3>
<p>It would also be unfair to completely dismiss this revolutionary technology. LLMs definitely are useful for summarizing information, or querying information using natural language.</p>
<p>Depending on how the models were trained and what they were trained on, of course – assuming that <a href="https://www.theguardian.com/technology/2026/jan/24/latest-chatgpt-model-uses-elon-musks-grokipedia-as-source-tests-reveal">the well isn’t being poisoned</a> with misinformation and lies. It’s a shame there isn’t more transparency about what models are trained on.</p>
<p>Anyway, here’s a few positive things I’ve found to be true about programming with the aid of LLMs and agentic systems:</p>
<ul>
<li>It is now much easier to get unstuck when programming.</li>
<li>It is now much easier to find information about any given subject.</li>
<li>It is now much easier to automate certain tasks.</li>
<li>You can talk with what resembles an intelligent system and reason about code using natural language.</li>
<li>Certain frustrating and repetitive tasks are now a thing of the past.</li>
<li>Getting stuck and losing an entire day to a silly issue is also super uncommon these days, as the LLM will likely be able to identify the issue immediately.</li>
<li>It is much easier to quickly iterate on a particular idea without losing a lot of time.</li>
</ul>
<p>All of these things I’ve just mentioned are genuinely great.</p>
<h3>Degrading Attention Span</h3>
<p>However, there are also a few caveats. The more you delegate, the shorter your attention span becomes. I can feel this happening to me in real time, the more I let agentic coding tools do the work for me.</p>
<p>The more I let work be done for me in larger chunks, the stronger the friction I feel when I do have some manual work or dive into the details.</p>
<p>Sometimes, I lack the information to keep up, too, and things are going too quickly.</p>
<p>At that point, there’s a big risk of becoming lazy. Yup, I mentioned this in my previous post about vibe coding, too.</p>
<div class="story" style="background-color: #EEE; padding: 5px 20px; border-radius: 15px; margin: 30px auto;">
<p>Okay, story time.</p>
<p>When I was a young student, I was really good at remembering things and understanding what was being taught, immediately, during class. Often, I actually understood everything. When tested, I was able to scrape by without actually needing to study most of the time, sometimes even with excellent grades.</p>
<p>But understanding and memorization are two different things. The truth is that this experience did not prepare me for a reality where at some point, I did, in fact, need to start studying and doing rote memorization.</p>
<p>The memorized knowledge would become part of a different creative and practical process, where this rote memorized knowledge would become something I could “invoke”. There’s connections in the brain that are now leveraging this knowledge and you become more capable as a result. It requires memorization and understanding.</p>
<p>Unfortunately, I did not know how to study. Some others had learned how to study because they couldn’t do what I did. But when I needed to really memorize things… it turns out I was really bad at that. With more complex subject matter, when actually tested, it would become clear that my knowledge and understanding was, in fact, not substantial enough.</p>
<p>That was a painful realization.</p>
</div>
<p>Using AI reminds me of this. I can feel myself becoming lazier, the same kind of laziness that prevented me from properly studying. I thought my grasp on things was “good enough”, but the truth is – it wasn’t.</p>
<p>Similarly, my coding discipline isn’t where I want it to be with this new technology. “Well, what’s being generated is good enough,” I can hear myself think, sometimes. I have to fight this laziness, because the temptation is there. It’s all part of the human condition, too – it’s a universal thing. If we can avoid the work, we will avoid it.</p>
<p>This is where discipline is so very important, too.</p>
<p>As soon as a moment comes when I let my guard down, I’m letting the overall quality of what I work on and ship slide, and that’s not okay.</p>
<p>Perhaps I did not properly check if all of the tests are solid, perhaps I relented on a particular implementation. Perhaps I skimmed over some architectural details that will seriously be an issue down the line.</p>
<p>Maybe it’s not a big deal. Maybe I can tell the agent to fix it when it becomes an issue. Sure, perhaps that’s true. But I don’t think that’s a good thing, especially when it comes to foundational stuff that you might be building.</p>
<p>(Foundations, as they are, should be laid in such a way that there is no doubt about the quality of what we are building upon.)</p>
<p>As long as I keep asking the right questions, I can avoid these various pitfalls, it’s true. But are these agentic systems good enough, even if we keep ourselves in check? I think a bunch of people have decided that, yes, they are.</p>
<p>I mostly agree, I think. But at the same time, I think it’s still a topic that’s being widely discussed because the degree to which we trust these agents definitely depends. For some, it’s mostly about productivity. For others, like me, it also becomes more existential, since a computer now competes with me for my expertise.</p>
<p>I think that’s the argument that people are now trying to have. “Is the randomized, unpredictable but mostly correct generated code good enough to ship? Should I even be programming?”</p>
<h3>Knowledge Erosion</h3>
<p>Despite the objective reality we live in, I have, as you can probably tell, various broad concerns.</p>
<p>I suspect that continued usage of LLMs and agentic coding will likely lead to problems down the line, when we don’t properly understand the consequences of cutting corners.</p>
<p>What I don’t want to do is just hand-wave that we no longer need to properly understand our own projects; because, let’s be real, I think that’s the direction we are going. I think we are genuinely at risk of losing people who actually know what they are doing.</p>
<p>Okay: perhaps not today, but a decade or two, three from now? It might be a problem, don’t you think so? It’s hard to guess when it will become an issue, too. It might be an issue half a decade from now, if a large percentage of developers get laid off.</p>
<p>One other particular immediate risk also exists: if we automate away many key skills, it is likely that developers will not learn as much as they used to; what they learn to do is rely on the machine to be productive.</p>
<p>A direct impact is that certain benefits to having this knowledge be “available” for direct use, are now no longer present. We will need to rely on automation even more. And so on. Our knowledge, and as such, agency, actually erodes away, like this.</p>
<p>Plus, in this scenario a couple of decades in the future, we might be totally reliant on this technology to get even the simplest things done.</p>
<p>Without all of the domain knowledge available, people will need the large language models and agentic systems to comprehensively be able to rationalize about a project. With potentially thousands of lines of code as part of a project, I’m unsure if these systems will be able to appropriately understand what’s going on with a limited context!</p>
<p>There’s a chance that domain knowledge experts are at risk of being laid off or fired, and once they’re gone, they take a certain amount of knowledge with them, and that is something which will cause problems down the line.</p>
<p>Perhaps this is a technological problem that can be solved by throwing more GPUs at the problem. Context windows are growing, after all.</p>
<h3>Sovereignty &amp; Independence</h3>
<p>But that brings me to another major point of contention for me – at some point we will be completely reliant on a third-party system that we cannot control, unless we can do some of this locally.</p>
<p>(I’ve tried, agentic work isn’t possible locally right now, as system memory is a strong limiter when it comes to handling longer context windows.)</p>
<p>I know this is a non-issue for many people who do not lack the funds to finance the many thousands of tokens they are using up to build the thing they’ve been aching to build, but for some, it is an issue. Folks who don’t have hundreds of dollars to throw at a problem when it comes to building their hobbyist project.</p>
<p>Also, especially lately, the idea of sovereignty has been very much on my mind. As a European citizen, after all, I am not blind to the technological and geopolitical shift that is currently occurring.</p>
<p>A question I ask myself very frequently now is: do I want to be dependent on <em>[insert technology or company here]</em>? If a technology is open source, that concern is minimal, but key software we use… isn’t.</p>
<p>This is a concern because on the geopolitical stage, things are not the way they used to be. That much has become abundantly clear this month. I am also deeply saddened by the many quality control issues in software recently, which has led me to examining alternatives to much of the software I use on a daily basis.<sup id="fnref:enshittification"><a class="footnote-ref" href="#fn:enshittification" role="doc-noteref">4</a></sup></p>
<p>The big elephant in the room (to be) is the big “AI” companies. OpenAI, Anthropic, xAI… their impact is enormous, and 100% American. So a question that plagues my mind is this: how reliant do I want to be on these American technology companies that are effectively leasing “programmer productivity” to us?<sup id="fnref:paying"><a class="footnote-ref" href="#fn:paying" role="doc-noteref">5</a></sup></p>
<p>It’s not just the “AI” companies, either. Apple, Microsoft, Google… the companies who maintain the operating systems on virtually all computing hardware used for productivity and communication – do we trust them to make the right decisions?</p>
<p>From what I can tell, many of the founders of these big corporations seem perfectly happy to cozy up to a government and president who seem to be isolating their country on the world stage.</p>
<p>If that’s how things are, isn’t being reliant on technology, whether it’s an operating system or an LLM or an agentic coding tool, a substantial risk for non-Americans, if they continue to isolate themselves from the international stage?</p>
<p>From what I can tell, America is certainly going through some turbulent times and certain platforms seem to be leaning very much in a direction that we would consider extremist here in Europe, with what used to be Twitter being a really great example.</p>
<p>(I don’t use the platform anymore because it has gotten worse, plus, I’m frankly super tired of the “AI push” that is super common on the platform.)</p>
<h3>Conclusion</h3>
<p>Being able to do the work myself without assistance definitely makes me a better developer, so I think it is important to be careful about these new tools.</p>
<p>My personal priorities regarding are as such:</p>
<ul>
<li>I should keep up-to-date with recent developments. LLMs and agentic tooling are very useful.</li>
<li>If possible, I should be able to run the majority of my workflows locally. (I might be able to build tooling to assist with local development? Running proper workflows locally is very difficult with limited hardware, however.)</li>
<li>I should make sure I do not rely on these tools too much, or become lazy.</li>
<li>I should focus on doing proper documentation when it comes to prompts, best practices, guidelines, etc.</li>
</ul>
<p>My nuanced takeaway is as such:</p>
<ul>
<li>Yes, LLMs and agentic workflows can be really useful. They can help us clear roadblocks and get informed faster than ever.</li>
<li>Because LLMs are trained on existing code, it is easier than ever to reproduce existing code. As such, “template code” will be easier to produce.</li>
<li>That does not mean we should be afraid to “get our hands dirty” and do some boring work ourselves. This is also part of what makes working on a project more personal. You have your own idea of what to do (and not to do) and how to do it. You can make technical and practical considerations and <em>you own them</em>, which makes what you craft unique to you.<sup id="fnref:brando"><a class="footnote-ref" href="#fn:brando" role="doc-noteref">6</a></sup></li>
<li>This new interaction paradigm offers various benefits, some of which we have not grasped yet and will no doubt delight and frighten us.</li>
<li>We should be careful about how we use this new technology and how much we rely on it, and where we source our compute. (For example: running models offline, locally, is the safest for our personal data, especially because of potential prompt injection attacks. We should be very careful about what data we expose to these models, and what access we grant them.)</li>
</ul>
<p>So, in short: there’s a lot to be careful about, but the benefits of using these systems are probably here to stay, and they are very much real. But since I care about my work, I will be very careful about how I use these new technologies.</p>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:foundations" role="doc-endnote"><p>A few years ago, I wrote about the importance of foundational work in my brief blog post called <a href="https://nicoverbruggen.be/blog/foundations">Foundations</a>. I rather like the story of the merchant and the builder.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:foundations" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:decision" role="doc-endnote"><p>We know that presenting an individual with a choice is something that will actually exhaust the brain. The more choices that are available, the more likely an individual is not to choose at all, or to get stuck.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:decision" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:microslop" role="doc-endnote"><p>I recently blogged about <a href="https://nicoverbruggen.be/blog/goodbye-windows">abandoning Windows</a> for a variety of reasons. Copilot was one of them, but so was the quality of the product. Earlier this month, people were even <a href="https://www.neowin.net/news/microsoft-confirms-some-windows-11-pcs-cant-shut-down-hibernate-after-latest-patch-tuesday/">unable to turn off</a> their computers from the Start Menu after a botched Patch Tuesday, so perhaps using “agentic coding” to improve Windows isn’t really going that well at <del>Microslop</del> Microsoft (apologies, I had to make the joke). Supposedly, they are focusing on getting things right this year. We’ll see. The proof is, as they say, in the pudding.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:microslop" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:enshittification" role="doc-endnote"><p>We might refer to this as “enshittification” and it seems to be everywhere. I’m working on a blog post about that, too.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:enshittification" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:paying" role="doc-endnote"><p>Remember, to get decent results you need to pay at least about ~20 dollars per month, and you have pretty hefty limits applied to your usage. You’re better off if you can run some models locally, but then you are limited in terms of speed and context length. Not ideal!&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:paying" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:brando" role="doc-endnote"><p>As the writer Brandon Sanderson explains: “you are the art”. He made an interesting presentation on <a href="https://www.youtube.com/watch?v=mb3uK-_QkOo">the topic of AI art</a> that I highly recommend you watch.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:brando" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sat, 31 Jan 2026 13:45:00 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[Hello, Bazzite!]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/hello-bazzite" />
        <id>https://nicoverbruggen.be/262</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>Welcome to the second part of my Windows-to-Bazzite journey. In my previous post I explained that I had decided to install <a href="https://bazzite.gg/">Bazzite</a>, a <a href="https://www.fedoraproject.org/">Fedora</a>-based Linux distribution, because after all, why not take Linux seriously?</p>
<p>(You may recall in the previous post I pointed out that, well, we might have Stockholm-syndromed ourselves into believing that debloating an operating system that disrespects you is a good choice to make. It is not.)</p>
<p>There are, obviously, various options when it comes to modern gaming on PC hardware. I briefly examined <a href="https://nobaraproject.org/">Nobara</a>, <a href="https://cachyos.org/">CachyOS</a> and <a href="https://store.steampowered.com/steamos/">SteamOS</a> before deciding and committing to Bazzite.<sup id="fnref:omarchy"><a class="footnote-ref" href="#fn:omarchy" role="doc-noteref">1</a></sup></p>
<p>The main appeal of Bazzite, to me (compared to these alternatives) is primarily the <a href="https://docs.bazzite.gg/Gaming">focus on gaming</a> and the OCI container based approach which effectively provides a sort of immutable distribution via <a href="https://github.com/bootc-dev/bootc">bootc</a>.</p>
<p>This approach makes the system more resilient, and less prone to accidental breaking. Also, it seems like an increasingly popular option. It’s also easy to use. Though I suppose whether this is actually true depends on the person sitting behind the monitor, doesn’t it? I’d say Bazzite is not too difficult to use, though.<sup id="fnref:kip"><a class="footnote-ref" href="#fn:kip" role="doc-noteref">2</a></sup></p>
<p>You may find that many of the things I describe below require some level of tinkering for optimal results, but Windows requires a similar level of tinkering for equivalent results, too, if you want a clean and debloated setup. I do want to point out that the Steam Deck experience is great <em>out of the box</em> for many games, so I probably will not have many issues there…</p>
<p>Now, I have tinkered with Linux before, even when I was a teenager. Those were the days. I daily drove Ubuntu, Mint, Fedora… I broke things, I’ve customized to my heart’s content. That was the past. These days, I want something that <em>just works</em>.</p>
<p>And increasingly, Windows ain’t it. But I also wasn’t keen on a Linux setup that requires constant attention.</p>
<p>If I want to, I can always tinker on a less important machine or in a VM (or on a mini PC, like I did <a href="https://nicoverbruggen.be/blog/desktop-linux">last time</a>). In the unlikely event that I do I need more flexibility on my current system, I can always spin up a <a href="https://docs.bazzite.gg/Installing_and_Managing_Software/Distrobox/">Distrobox</a> with my preferred system or use <a href="https://docs.bazzite.gg/Installing_and_Managing_Software/Homebrew/">Homebrew</a>. I will elaborate on that in a bit.</p>
<p>Like most other distros, you must first decide what desktop environment (DE) you wish to use with Bazzite. Some distros give you a big disk image file so that you can pick a desktop environment during the installation process.</p>
<p>Bazzite ships dedicated images based on the desktop environment and whether you need the Steam Big Picture mode upon startup (the latter being the so-called “deck” variant, because Steam Deck ships with something similar on SteamOS).</p>
<h3>Desktop environments</h3>
<p>I used to be a big fan of GNOME, but this was mostly during the 2.0 days, many years ago, when I was still a teenager with a small Dutch Linux blog. I did actually blog about GNOME 3 when it was brand new and was more impressed by it than Unity, which was what Ubuntu was offering at the time.</p>
<p><img src="/media/blog/20260215_125401_884747/source.png" alt="From the archives: an actual screenshot of my Ubuntu desktop, dated April 25, 2009. I used to run the operating system in Dutch at the time, and apps like Pidgin, Evolution and OpenOffice and were my go-to daily apps that I used while I was still in high school. The internet was a different place at the time. I was quite fond of Ubuntu back then." /></p>
<p>Since revisiting Linux on the Steam Deck and trying out Fedora, more than a decade after last daily driving Ubuntu, I’ve grown quite fond of KDE. I think it’s probably the nicest and most modern desktop environment right now, so that’s what I settled on. It’s probably also how I use computers: I don’t really use virtual desktops a lot.</p>
<p>GNOME seems to be more opinionated when it comes to managing windows and how it does things. I really should give it another proper go at some point, but since I was used to KDE I decided not to rock the boat too much, on this particular occasion, so I decided to stick with KDE for now.</p>
<h3>Choosing a flavour</h3>
<p>The KDE version of Bazzite that I was interested in has two particular flavours. I tried out both options, which are:</p>
<ul>
<li>
<p><strong>Bazzite</strong>: The first is a version that boots straight into a <em>traditional KDE desktop</em>. It is basically <a href="">Fedora Kinoite</a> with some tweaks and additions that make it better suited for gaming. Steam comes preinstalled, but it’s less of a console-like experience since you are required to sign in and switch to Big Picture manually.</p>
</li>
<li>
<p><strong>Bazzite (Deck)</strong>: The second is a version that boots <em>straight into a SteamOS-like mode</em>, ideal for a setup with a controller. This one works basically the same as SteamOS, but gives you the option to switch to the Desktop Mode, too, if you so please. Useful if your computer will primarily be used for gaming and you want to be able to use it more like a console.<sup id="fnref:gamescope"><a class="footnote-ref" href="#fn:gamescope" role="doc-noteref">3</a></sup></p>
</li>
</ul>
<h3>The experiment</h3>
<p>I decided that I was going to play all my PC games on Linux for the next month, no exceptions. A few years ago, I built a computer on the Zen 4 platform as a budget build. This is my so-called “secondary PC”, and it’s a little less powerful than my main PC, but good enough for a serious experiment.</p>
<p>For this experiment, I was not willing to risk breaking my primary PC, so I decided that I was going to do a test run instead. In particular, I decided to avoid dual booting on the same disk on my main machine, because I know that Microsoft has <a href="https://www.theverge.com/2024/8/21/24225108/microsoft-security-update-windows-linux-dual-boot-errors">a history of breaking dual booting with updates</a>. I should point out that dual booting is not an issue if you use two different storage drives.</p>
<p>Since I did want to commit to the test run, I unplugged my main desktop computer. I then moved it out of the way, and put the secondary PC in its place. <strong>For the next month, this PC running Bazzite was going to be my main gaming and general purpose computer instead.</strong><sup id="fnref:coding"><a class="footnote-ref" href="#fn:coding" role="doc-noteref">4</a></sup></p>
<p>When I initially built this secondary PC, it didn’t even have a discrete graphics card. This was during the crypto boom in 2021, when GPUs were basically way too expensive, so I used it with integrated graphics for a while.</p>
<p>(In retrospect, given the current price surges for computer parts affecting memory, storage <em>and</em> graphics cards, I suppose we didn’t have it too bad back then… if only we had known what the pending AI boom would take from us…)</p>
<p>A year or two ago I upgraded the GPU in my primary computer, and moved my older GPU to the secondary PC. With a dedicated GPU, this computer was now easily capable of 1440p (or even 4K) gaming when using upscaling technology, like FSR.</p>
<p>I installed Bazzite (the desktop variant) and got to work. I installed <a href="https://zen-browser.app/">Zen Browser</a> and got rid of Firefox, and decided to check a few games. I also used <a href="https://github.com/Vysp3r/ProtonPlus">ProtonPlus</a> to install the latest version of ProtonGE, which improves compatibility with certain games beyond what Valve has on offer with their version of Proton.</p>
<p>To verify whether this switch was going to even be viable, I wanted to check out some of my go-to games and see if they ran without issue. I also wanted to see what I was able to do with the command line and install developer tooling. In short, I wanted to assess both productivity and gaming on this new system.</p>
<blockquote>
<p><strong>Before we begin</strong>, I should note that the latest, up-to-date documentation can be found at <a href="https://docs.bazzite.gg/">docs.bazzite.gg</a>, which is excellent. The older this blog post is, the more likely some of the information will be out-of-date. If something doesn’t work, take a look at the documentation to find out what has changed.</p>
</blockquote>
<h2>Part 1: Productivity on Bazzite</h2>
<h3>Software availability and management</h3>
<p>Due to the immutable nature of Bazzite, you can’t just install packages like you would on a traditional Fedora installation, since the system partition cannot be written to.</p>
<p>So, if you’re a PHP developer like me, you can’t just run <code>dnf install php</code> – that’s the equivalent of <code>apt install php</code>, for you server folks familiar with Ubuntu. Doing so results in the following output:</p>
<pre><code class="language-sh">nico@bazzite:~$ dnf install php
ERROR: Fedora Atomic images utilize rpm-ostree instead (and is discouraged to use).
Please, read our documentation
https://docs.bazzite.gg/Installing_and_Managing_Software/
</code></pre>
<p>You do have some options, it isn’t like you’re out of luck. In fact, the way this model works is quite similar to macOS; you cannot update system libraries and binaries, but you can install your own in your user folder.</p>
<p>Bazzite actually ships with <a href="https://docs.brew.sh/Homebrew-on-Linux">Homebrew</a> and <a href="https://docs.bazzite.gg/Installing_and_Managing_Software/Distrobox/">Distrobox</a>, both of which give you the option to install software and use it, while retaining the immutable base install. Users familiar with macOS will also likely be familiar with Homebrew, which has excellent availability of certain formulae, even for Linux.</p>
<p>This is very useful for software developers, but if you are looking to install other applications (e.g. <a href="https://www.libreoffice.org/">LibreOffice</a> or some other GUI apps) you should probably be looking at Flatpaks (via <a href="https://github.com/kolunmi/bazaar">Bazaar</a>). Apps you install this way are usually sourced from <a href="https://flathub.org/en">Flathub</a>, unless you specify another source provider.</p>
<p>Let’s go over a couple of options that are available as an alternative to using a traditional package manager in more detail, in order of preferability on this particular operating system the an immutable base:</p>
<h4>Flatpaks</h4>
<blockquote>
<p>Flatpak is a universal containerized package format that tries to sandbox applications through flexible permissions that the application has access to on your system.</p>
</blockquote>
<p>This is our first option. If at all possible, using Flatpaks is actually the most preferable option, too.</p>
<p>There’s a couple of benefits to Flatpaks; first off, Flatpaks are isolated by default, which means that they can’t just access every file on your system. So applications installed via <a href="https://flathub.org/en">Flathub</a> for example are probably going to be pretty decent citizens.</p>
<p>Bazaar also shows how many permissions individual apps require to work correctly, so you can assess how much of a security risk they might pose and what you are comfortable with.</p>
<p>By doing so, Flatpaks employ a similar approach to apps you can find on the Mac App Store; i.e. they are sandboxed by default, they ship with their own dependencies, and are decoupled from system and system library updates.</p>
<p>In case you want to tweak what permissions are granted to a given Flatpak, you can also use a tool like <a href="https://github.com/tchx84/Flatseal">Flatseal</a>. (I actually needed this to fix a permission at once point when something was misconfigured or I wanted to grant access to a particular directory that wasn’t included by default. It’s good to have the option!)</p>
<h4>Homebrew</h4>
<blockquote>
<p>Homebrew is a package manager that installs packages into their own prefix. It is primarily used for command-line interface (CLI) and terminal user interface (TUI) applications. Homebrew can also install graphical applications using the <code>--cask</code> flag, but most are for macOS as support for casks on Linux is still developing.</p>
</blockquote>
<p>As the developer of <a href="https://phpmon.app/">PHP Monitor</a> for macOS, I am rather familiar with Homebrew, as I use it daily on my Mac. It’s very nice to see it is available on Linux, too. You can easily install various dependencies and developer tools with Homebrew.</p>
<p>For example, I used it to install my PHP stack (alongside <a href="https://getcomposer.org/">Composer</a>), which is one of my most frequently used programming languages:</p>
<pre><code>brew install php@8.5 composer
</code></pre>
<p>This allows me to set up my Bazzite machine for PHP development, without needing to use Distrobox or layering packages.</p>
<p>I should note that I am actually considering building the equivalent of PHP Monitor for Linux, but that’s a story for another time. I’d need appropriate funding for that, and that currently isn’t an option. But maybe, some day.</p>
<p>In the meantime, if you’d like to contribute to increase the odds of a Linux version of PHP Monitor, visit my <a href="https://nicoverbruggen.be/sponsor">sponsorship page</a> and let me know that you pledged with the intent to make a Linux version possible.</p>
<h4>Quadlet</h4>
<blockquote>
<p>Quadlet is a feature of <a href="https://podman.io/">podman</a> that allows a user to run a container as <a href="https://systemd.io/">systemd</a> units. It works by using a declarative syntax like <a href="https://docs.docker.com/compose/">docker compose</a> but integrates to systemd and uses podman as a backend.</p>
</blockquote>
<p>I haven’t had the time to check this out, as I’m not using my machine to spin up certain services like <code>nginx</code> and such, but you can find out more documentation about what Quadlet is and <a href="https://docs.bazzite.gg/Installing_and_Managing_Software/Quadlet/">how to use it</a> via the official documentation.</p>
<h4>Distrobox</h4>
<blockquote>
<p>Run other minimal variants of popular Linux distributions in Bazzite inside of a container, and access each distribution’s packages without any of their dependencies and libraries affecting the host machine.</p>
</blockquote>
<p>If you want even more control, you can opt for a container-based alternative in Distrobox. It lets you install a lightweight distribution in a container which you can use to install applications. There’s even a GUI included with <a href="https://github.com/ranfdev/DistroShelf">DistroShelf</a>.</p>
<p>You can use Distrobox even for applications that require a GUI, so if you have a specific application that you can only install on, for example, Ubuntu, you can still get it working on Bazzite.</p>
<p>Also, you can run whatever <code>apt install</code> command you’d like, if you know it’s available on Ubuntu… or you can source a PPA (personal package archive) of your choice. This affords you a certain degree of flexibility, for sure.</p>
<p>Certain developers only distribute official packages for the most popular Linux distributions, and that sometimes that ends up being just Ubuntu. It’s nice to be able to install said software without any issues.</p>
<p>Containers are not intended to be fully persistent, so keep that in mind. I should note that Distrobox is also great for distrohoppers, who may find it useful.</p>
<h4>AppImage</h4>
<blockquote>
<p>AppImage is a universal package format that attempts to bundle every dependency that an application needs into one portable file. They can be installed by downloading any file with a <code>.AppImage</code> extension and then giving it executable permissions in the file’s properties so the application can run properly. Like Windows executable files, they are usually found on the project’s website to download.</p>
</blockquote>
<p>Like Flatpaks, AppImage files are standalone packages that will let you run the software, but unfortunately requires manual updates.</p>
<p>For example, I use an AppImage for <a href="https://lmstudio.ai/">LM Studio</a>, which is otherwise unavailable via other channels. Plenty of applications offer an AppImage version, but sometimes you do actually need to install additional dependencies to get them to work.</p>
<p>To troubleshoot these, you can start the AppImage via the terminal, which will display any potential warnings or error messages. In my experience, using these can be a mixed bag.</p>
<p><strong>Tip</strong>: You can install <a href="https://github.com/mijorus/gearlever">Gear Lever</a> to make managing AppImage packages easier. Especially updating them becomes less tedious when configured correctly.</p>
<h4>rpm-ostree</h4>
<blockquote>
<p>Bazzite utilizes <code>bootc</code> to manage the base image for your system, pin specific versions, and perform rollbacks when needed. For systems with customized software via layered packages, <code>rpm-ostree</code> becomes essential for installing, upgrading, and managing those additions. (<a href="https://knowledgebase.frame.work/en_us/how-to-manage-bazzite-with-bootc-and-rpm-ostree-SJ8bn4vhke">Source</a>)</p>
</blockquote>
<p>If you do need to install some software that requires deep system integration, you may need to still layer packages (on top of the existing base, read-only container). This is accomplished using <code>rpm-ostree</code>.</p>
<p>I should note: this is a bit of a potentially annoying process in the sense that you may need to undo these changes if you plan on doing upgrades to the base container, so you probably only want to layer packages if it isn’t possible to use a Flatpak version or if that sandboxed version has too many limitations.</p>
<p>(For example, any given password manager installed via Flatpak will not be able to integrate with the browser to the same degree as a regular package version installed at the system level. While there are workarounds, the easiest solution is sometimes just to layer a package.)</p>
<p>Here’s another example: I have layered <code>gsmartcontrol</code> on top of my main installation. <code>gsmartcontrol</code> accesses disk health information (SMART) and more, which requires it to be layered for it to work correctly.</p>
<p>You can see I did this by running <code>rpm-ostree status</code>, like this:</p>
<pre><code>nico@bazzite:~$ rpm-ostree status
State: idle
Deployments:
● ostree-image-signed:docker://ghcr.io/ublue-os/bazzite:stable
                   Digest: sha256:71a51c4faf8cec6401b8c99ab319d1e7705a77f2253817710093eb3b436132cd
                  Version: 43.20260101 (2026-01-02T00:07:13Z)
          LayeredPackages: gsmartcontrol

  ostree-image-signed:docker://ghcr.io/ublue-os/bazzite:stable
                   Digest: sha256:4b712efdc9b716aaef9b80222868bb8d6121f24f84f57c45c210481283b04c36
                  Version: 43.20251231.1 (2025-12-31T23:48:57Z)
          LayeredPackages: gsmartcontrol
</code></pre>
<p>As an aside: as I mentioned before, one of the advantages of using this containerized system is that you can easily revert to a previous deployment.</p>
<p>You can either revert via the terminal:</p>
<blockquote>
<p>A rollback to the previous system deployment can be done by <strong>entering this command in a host terminal</strong>: <code>rpm-ostree rollback</code>.</p>
</blockquote>
<p>Alternatively, you can revert via the boot menu:</p>
<blockquote>
<p>Rollback can also be done in the GRUB menu (the menu you see before booting into Bazzite on Desktop images) by choosing the previous boot entry before booting to the desktop. It shows your current (<code>:0</code>) and your previous (<code>:1</code>) deployments. Your personal files will <strong>not</strong> be affected by this, and you can still update to the newest builds after rolling back.</p>
</blockquote>
<p>You can learn more <a href="https://docs.bazzite.gg/Installing_and_Managing_Software/rpm-ostree/">about <code>rpm-ostree</code> here</a>. The documentation also explains how to pin a particular deployment.</p>
<p>I’ve never needed this, but if you need a fixed environment, pinning a deployment may be very useful. I suspect people who want a consistent environment for benchmarking or testing may find this feature super helpful, or folks who keep their machine offline for an extended duration may also benefit.</p>
<h3>Software availability on Bazzite</h3>
<p>My most commonly used applications are available on Bazzite via either Flatpak or AppImage. In no particular order, these are applications that I use:</p>
<ul>
<li>Zen Browser (my preferred browser, based on Firefox)</li>
<li>VSCodium (Visual Studio Code alternative)</li>
<li>Discord (voice chat and screen sharing with friends)</li>
<li>OBS Studio (streaming and recording software)</li>
<li>LM Studio (app to run local LLMs)</li>
<li>FreeCAD and Blender (modelling software)</li>
<li>FontForge (a font editor)</li>
<li>Obsidian (markdown text editor &amp; knowledge base)</li>
<li>LibreOffice (office suite)</li>
<li>CrealityPrint (slicer for my 3D printer)</li>
</ul>
<p>Some of the software that I pay for is also available on Linux:</p>
<ul>
<li>1Password (a password manager)</li>
<li>PhpStorm (IDE for PHP)</li>
<li>Sublime Text (an excellent text editor)</li>
<li>Sublime Merge (my favorite Git GUI)</li>
<li>REAPER (a digital audio workstation)</li>
</ul>
<p>There are a few applications that I really enjoy on macOS that I do not have available on Linux, but really there aren’t that many. This was very different a decade ago.<sup id="fnref:macos_apps"><a class="footnote-ref" href="#fn:macos_apps" role="doc-noteref">5</a></sup></p>
<p>I can also still run some Windows software via WINE and Proton, too. I’m not saying every program will work, but WINE is good enough that many programs will now run without too many issues. Often, running software under Proton or using <a href="https://github.com/Open-Wine-Components/umu-launcher">umu</a> actually yields better results in terms of compatibility, the older the software is.</p>
<p>Certain games that aren’t available via Steam can also be installed via Lutris or the Heroic Games Launcher. To give you a practical example: I was able to get StarCraft II and Battle.net up and running this way, for example.</p>
<p><img src="/media/blog/20260209_155927_459739/source.jpg" alt="Certain Windows applications can run just fine on Bazzite, including launchers for games. StarCraft II takes me back to my college days, to 2011-2013, the glory days of that game. I’m in the middle of replaying the single-player campaign, actually…" /></p>
<p>That reminds me… let’s talk about gaming. After all, it’s what I use my desktop computer for the most, since most of my productivity work (still) happens on the Mac. Maybe that’ll change in the next little while, who knows?</p>
<p>So, without further ado…</p>
<h2>Part 2: Gaming on Bazzite</h2>
<h3>Proton all the way down</h3>
<p>For the majority of the games available on Steam that you may want to run on Bazzite, the compatibility layer, <a href="https://en.wikipedia.org/wiki/Proton_(software)">Proton</a>, will be used.</p>
<p>It effectively lets you run Windows games on Linux. A couple of years ago, the truth was that “a few games run on Linux”, and now “a few games don’t run on Linux”. I think that’s a very interesting shift; I generally don’t have to worry about games not running.</p>
<p>I should note that <strong>certain games will likely never be compatible</strong>, and you should keep that in mind; in particular, games requiring kernel-level anti-cheat protection measures, like the latest Battlefield game, won’t run. (However, I should note that <a href="https://areweanticheatyet.com/">not all hope is lost</a>.)</p>
<p>You can learn more about game compatibility on Steam’s store pages. If you want more information and potentially tinker to get even more out of your setup, you can also consult third-party sites like <a href="https://www.protondb.com/">ProtonDB</a>, which contain helpful user reported data.</p>
<p>To be perfectly blunt, if you want to be able to run all of your games your best bet is to maintain a dual-boot setup right now; you can run the majority of games on Bazzite, and a handful of unsupported games on Windows.</p>
<p>(Alternatively, like me, if you don’t play any of those games, you can simply ignore games that are not compatible. I still have a small Windows partition in case I bump into a game-breaking bug, as a fallback option. But so far, I haven’t needed it, and fingers crossed that things remain this way.)</p>
<h3>Launch option tweaking</h3>
<p>Let’s get another important thing out of the way first: unlike on Windows, where you have a separate driver and application to use your GPU and enable various features like image sharpening, setting frame rate limits, etc., you don’t have this on Linux.</p>
<p>You do <em>not</em> have software that needs to be installed to get things up and running on Linux. Drivers don’t work the same way on Linux, that’s just the bottom line. Linux-based distributions usually ship with <a href="https://mesa3d.org/">Mesa</a>, which is an open source driver for various GPUs. Well, sort of.</p>
<p>This does also mean that there is no GUI for some of this stuff. You might be used to this on Windows with GeForce Experience or AMD Adrenalin. In those apps, you can easily tweaks settings of your GPU, even to the point of setting up overclocking or undervolting. Those apps are not available on Linux.</p>
<p>But not to worry: if you do want to tinker around you do have <em>some</em> options. Here’s a few useful applications that you may want to use to tinker around with your precious and expensive hardware:</p>
<ul>
<li><a href="https://flathub.org/en/apps/io.github.ilya_zlobintsev.LACT">LACT</a> allows you to view and tweak your GPU’s frequencies and power limits. The app also contains tons of useful information about your GPU. If you want to, you can also use it to overclock (or underclock) your device.</li>
<li><a href="https://flathub.org/apps/io.github.benjamimgois.goverlay">Goverlay</a> lets you tweak MangoHUD (an overlay that displays FPS information and temperatures) and allows you to apply sharpening and other effects via VkBasalt.</li>
<li><a href="https://flathub.org/apps/io.github.radiolamp.mangojuice">Mango Juice</a> is another application that lets you customize the MangoHUD overlay. By default, on the Bazzite Deck image, SteamOS provides an easy toggle for the overlay – you might be familiar with that from the Steam Deck.</li>
</ul>
<p>Despite that, you may still need to apply these so-called “launch options” on a game by game basis, which will allows you to fix various issues or do specific things like apply a frame limit or sharpening.</p>
<p><img src="/media/blog/20260209_163149_603831/source.png" alt="Setting up launch options looks like this. In this screenshot, for example, I force using Wayland, and use my second monitor as the “primary monitor” for running the game, which can be useful for certain games that don’t respect which monitor you’ve set as the primary one. This is even an issue on Windows, where you basically can’t fix that – thankfully, you can here. For the final configuration, take a look at the “Some Warhammer woes” section below." /></p>
<p>In order to avoid issues with <a href="https://store.steampowered.com/app/1142710/Total_War_WARHAMMER_III/">Total War WARHAMMER III</a> and getting a more-or-less consistent framerate with the RX 6800 XT that’s inside my Linux machine, I used the following parameters – a 90 FPS cap and I also enabled MangoHUD (but hidden by default); this allows me to toggle the MangoHUD overlay when I want:</p>
<pre><code class="language-shell">DXVK_FRAME_RATE=90 \
MANGOHUD_CONFIG=&quot;no_display&quot; \
mangohud %command%
</code></pre>
<blockquote>
<p><strong>Note</strong>: <code>DXVK_FRAME_RATE</code> has been deprecated now. It’s now advisable to limit framerate with something like <code>mangohud</code> instead. I encountered some more issues with this game, and my final fix with this new approach is listed a little further down in this post.</p>
</blockquote>
<p>I was a bit worried that this meant I was going to need to switch back to Windows, but fortunately this configuration seems to work fine for me.<sup id="fnref:forcing_proton"><a class="footnote-ref" href="#fn:forcing_proton" role="doc-noteref">6</a></sup></p>
<p><img src="/media/blog/20260209_162159_370132/source.jpg" alt="Malus Darkblade is my favorite Dark Elf legendary lord, because he’s a powerhouse. After setting up the game with a couple of extra parameters and forcing the Proton build, I get a pretty decent experience." /></p>
<p>Performance is also pretty decent, too, but keep in mind that I am using ProtonGE 10. Depending on the version of Proton that you’re using and the version of Mesa (the drivers) that ship with your distribution, your mileage may vary.</p>
<p>To be fair: I remember trying this a few months prior, before the summer, and WARHAMMER III was significantly slower and for me that was a bit of a dealbreaker. Fortunately, that’s no longer the case.</p>
<p><img src="/media/blog/20260209_155836_861007/source.jpg" alt="I should point out that you can just use Big Picture and not worry about tinkering with your games, too. Many games are supported out of the box and will just run." /></p>
<p>I should point out that none of this tinkering is generally <em>required</em> but it can improve your experience running games on Bazzite. I was seeing improved performance with WARHAMMER, but I generally I don’t have to tweak many things.</p>
<p>Sometimes, though, I do need enable a frame limiter. On Windows, you can enable this in the various GPU app config panels (NVIDIA Control Panel/GeForce Experience, AMD Adrenalin) but on Linux there’s no direct equivalent.</p>
<h3>Limiting Framerate</h3>
<p>I was playing Deponia some time ago, but that had some known issues when running on displays with high framerates. This is a platform-agnostic bug, by the way. It’s a problem with how the game was built.</p>
<p>So, in order to be able to properly play the game, I would need to limit the game’s frame rate to 60 FPS. You have a few options for framerate caps; you
can use external tools like <code>mangohud</code> or on the level of DXVK (which is, as I have mentioned before, no longer advisable).</p>
<p>I was not willing to cap my monitor’s framerate to 60 Hz, which would also have fixed this problem, so I looked into another option.</p>
<p><img src="/media/blog/20260209_160028_008859/source.jpg" alt="As you can see when you look at the overlay and the utilization of my machine, Deponia is not a taxing game. Because of that, it ran too fast – which broke the game. Fortunately, there’s an easy fix and you can see it in action, here." /></p>
<p>I decided to cap the game to 60 FPS because that ensures the game works correctly. For Deponia, I settled on frame limiting through <code>mangohud</code>:</p>
<pre><code class="language-shell">MANGOHUD_CONFIG=&quot;fps_limit=60,no_display&quot; mangohud %command%
</code></pre>
<p>To verify that the frame limiter works correctly, you need to press RIGHT-SHIFT and F12 to toggle the HUD, since it defaults to <code>no_display</code> by default given these parameters.</p>
<p>For games running on DirectX (and being translated with DXVK) you can use the following for a 60 FPS cap, since <code>DXVK_FRAME_RATE</code> was deprecated:</p>
<pre><code class="language-sh">DXVK_CONFIG=&quot;dxgi.maxFrameRate = 60; d3d9.maxFrameRate = 60&quot; %command%
</code></pre>
<h3>Modding Metaphor</h3>
<p>I also set up Metaphor ReFantazio with <a href="https://github.com/SpecialKO/SpecialK">Special K</a> and <a href="https://codeberg.org/Lyall/MetaphorFix">MetaphorFix</a> to address some performance issues that the game has at high rendering resolutions. I wanted to see if I could just as easily mod my games on Linux as I could on Windows.</p>
<p>Sure enough, it wasn’t that difficult!</p>
<p>For Metaphor, after doing some tweaks and capping the framerate to 90 FPS at native 4K, I was happy with the performance in the main city hub, which I remember from the demo being the most difficult to run in terms of performance.</p>
<p>I decided to set the following launch flags:</p>
<pre><code class="language-shell">WINEDLLOVERRIDES=&quot;dxgi=n,b&quot; # use custom DLL
MANGOHUD_CONFIG=&quot;fps_limit=140,no_display&quot; # limit FPS
mangohud %command% # also run mangohud
</code></pre>
<p>The idea is to cap the max framerate to 140 regardless, and use <code>dxgi.dll</code> override (Special K) to set an even lower framerate cap (90).</p>
<p>In Special K, I added <code>MetaphorFix.asi</code> from Lyall to the plug-ins section and set that up so that the ambient occlusion resolution is lowered, since running at 1x means running ambient occlusion at 4K, which is a bit expensive on the GPU.</p>
<pre><code class="language-txt">[Ambient Occlusion]
Resolution = 0.5
</code></pre>
<p>As an added bonus, the default configuration also skips the intro movies. Nice.</p>
<p><img src="/media/blog/20260209_160320_842017/source.jpg" alt="The Special K overlay for Metaphor lets you tweak all sorts of things. It’s super nice that it just runs correctly, even on Proton." /></p>
<p>I finished Metaphor a couple of days ago, and my entire playthrough was done on my machine running Bazzite. I had a blast!</p>
<h3>Setting up my favorite MMORPG</h3>
<p>When I initially set up Bazzite, I also installed <a href="https://store.steampowered.com/app/39210/FINAL_FANTASY_XIV_Online/">Final Fantasy XIV</a>, which I am launching with <a href="https://goatcorp.github.io/faq/steamdeck.html">XLCore</a> (which is the custom FFXIV launcher for Linux). At the end of last year, I completed <em>Endwalker</em>, and the last bits of the story I was able to complete on Linux.</p>
<p>I’d been (very slowly) making my way through the different expansions since 2020, and it took me about 5 years to make it to the end of the original decade-long story arc, finishing the game last year.</p>
<p><img src="/media/blog/20260209_134133_654056/source.jpg" alt="“Once we depart, there is no turning back.“ Well, truthfully, Lucia’s sentiment applies to moving to Linux, too. Who knew." /></p>
<h3>Some Warhammer woes</h3>
<p>After a few sessions of WARHAMMER III I found out that the game has a tendency to crash every now and then, and it might not even be my fault. I theorized at the time that it was likely related to the Mesa driver… and it happens when the game loads back to the campaign map under some conditions.</p>
<p>It didn’t happen that frequently, but since it’s a GPU crash it took down the entire system. I found <a href="https://gitlab.freedesktop.org/mesa/mesa/-/issues/13856">an existing bug report</a> and chimed in with my own experience.</p>
<p>At the time of writing, I have not had WARHAMMER III crash on me, so it’s possible the issue was resolved by the developers; or perhaps an update to Mesa fixed the issue. There’s also a small chance I may still run into the issue at some point, of course.</p>
<p>I also found out how to do Radeon Image Sharpening (or CAS, “Constrast Adaptive Sharpening”) with <code>VK_BASALT</code>, which is a setting I often used on Windows.</p>
<p>My new launch options are as follows:</p>
<pre><code class="language-shell">PROTON_ENABLE_WAYLAND=1 # This forces Wayland
WAYLANDDRV_PRIMARY_MONITOR=DP-2 # This forces a specific monitor
DXVK_FRAME_RATE=144 # Limits the framerate
ENABLE_VKBASALT=1 # Enables CAS sharpening
%command%
</code></pre>
<p>The configuration file in <code>/home/nico/.config/vkBasalt/vkBasalt.conf</code> looks like this, effectively enabling CAS by default if VkBasalt is enabled:</p>
<pre><code class="language-conf">effects = cas
toggleKey = PgUp
enableOnLaunch = True
casSharpness = 0.4
</code></pre>
<p>In terms of performance, things are quite promising. I was seeing very similar performance to Windows. I was curious, and decided to do a direct comparison.</p>
<p>Keep in mind, I am using AMD hardware – performance with NVIDIA GPUs is generally up to 20% lower in DirectX 12 titles when compared to Windows, but they’re working on it to fix this.</p>
<p>I did some testing later, on the same machine. When benchmarking WARHAMMER III’s <em>Immortal Empires</em> (uncapped) at the same settings, on the same hardware, this resulted in an average FPS of ~140 FPS on Windows, and ~150 FPS on Bazzite.</p>
<p><img src="/media/blog/20260209_160632_042207/source.jpg" alt="Please note that Bazzite running Proton reports itself as Windows 10, which is why it says so in this screenshot. This is actually running on Linux." /></p>
<p>This is with no other applications running in the background. Notably, 1% and 10% lows were comparable with a delta of about 5 FPS on average.</p>
<p>So, all in all, things seem to run well, sometimes even better than on Windows (when using an AMD graphics card) and we can probably expect the NVIDIA drivers to catch up relatively soon. At that point the majority of people will be able to switch to Linux without needing to be worried about a performance hit.</p>
<h2>Conclusion</h2>
<p>I am now able to get some of my programming work done on my desktop computer AND play video games without many issues, so I really no longer need Windows.</p>
<p>You might be wondering how that “month of Bazzite” without using Windows went? Well, quite well! About two months into the experiment, I made the decision to stick with this and switch to Bazzite on my main computer, too.</p>
<p>I installed the “SteamOS-like” version of Bazzite on my secondary rig, and connected it to my 4K display, and now I have a console-like experience and a traditional gaming PC, both running slightly different flavors of the same OS, but both distinctly <em>not</em> Windows.</p>
<p>This effectively gives me a sneak peek at the experience many gamers will have when they purchase a Steam Machine later this year.</p>
<p>That is, of course, assuming the hardware is at least somewhat affordable, which is not a given, considering the current pricing madness of hardware components… some prices have quadrupled for certain components.</p>
<p>I do still have one SSD with Windows on it for games that exhibit strange issues or for games that require the OS specifically, but so far I haven’t needed it for anything. I set it up on a separate SSD so that I would not have bootloader issues after running Windows Update, so that’s something to keep in mind.</p>
<p>In all likelihood, I will probably eventually remove this partition, too, seeing as I haven’t needed it. It does provide a bit of peace of mind, so I’ll keep it around for another year or so, and if Bazzite continues to be a wonderful experience, I think it may be over and out for Windows in this household.</p>
<p>Truthfully, for all intents and purposes, it already is.</p>
<p>You can learn more about Bazzite or try it out by going to <a href="https://bazzite.gg/">bazzite.gg</a>.</p>
<p>I promise you, it’s worth a shot. You don’t need to disrespected by your operating system on a daily basis.</p>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:omarchy" role="doc-endnote"><p>I also briefly tried out <a href="https://omarchy.org/">Omarchy</a>, a sort of remix by <a href="https://dhh.dk/">DHH</a>, one of the most opinionated tech people and owner of 37signals, the company behind Basecamp. I applaud his enthusiasm about Arch Linux, but I can’t say this particular distribution is something for me.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:omarchy" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:kip" role="doc-endnote"><p>I think anyone who is interested in learning more about computers can benefit from setting up Linux on their computer. For regular people, I suspect the Steam Machine or Steam Deck are better choices; they don’t require any previous knowledge and things just work out of the box.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:kip" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:gamescope" role="doc-endnote"><p>Games run using <a href="https://github.com/ValveSoftware/gamescope"><code>gamescope</code></a>, which acts as the compositor in this default SteamOS-like mode. It’s pretty cool, since it lets you use FSR and supersampling out of the box by specifying scaled resolutions. This is currently not possible on the desktop without installing extra software like the Heroic Games Launcher or specifying that <code>gamescope</code> needs to be used via tweaked launch options.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:gamescope" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:coding" role="doc-endnote"><p>I should point out that I don’t do software development on these desktop computers. I have a Mac mini for that, which runs macOS. I can’t say I’m too happy about the direction of macOS over the last 24 months, but to be honest… Linux is becoming an increasingly enticing alternative. However, the reason I haven’t switched at this point is because I still believe macOS provides the most premium developer experience at this time. Should this change, I will certainly consider buying new hardware and switching. But right now, they are the uncontested #1, especially in combination with their M-series of chips on laptops, which sport industry-leading battery lifetime and incredible performance. Even my M1 MacBook Air (the cheapest model back then!) still gets many hours of screen-on time between battery charges, which is absolutely remarkable and not the norm in x86-land. A laptop that is five years old never aged this well!&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:coding" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:macos_apps" role="doc-endnote"><p>Some apps that I really loved on macOS simply no longer exist, like iTunes. (I thought it had a great UI for managing my offline music collection. Alas, it no longer exists, and the Apple Music app just isn’t the same.)&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:macos_apps" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:forcing_proton" role="doc-endnote"><p>I should note that in certain situations it is preferable to run the Windows build of a given game, despite a Linux build being available. For the various Total War games, I had a better experience running the Windows version, but that isn’t necessarily always a good idea. For example, Factorio is known to run a lot better on Linux – it is able to make auto-saves in the background without needing to pause the game, for example, which isn’t possible on Windows.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:forcing_proton" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sun, 15 Feb 2026 14:00:00 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[My 2025 Video Game List]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/video-game-list-2025" />
        <id>https://nicoverbruggen.be/261</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p class="headline">For the most part, 2025 was still very much a year of recovery for me. I was fortunate enough to be able to play plenty of games this year, mostly out of necessity because I needed something to do as I continued to recover from burnout.</p>
<p>In terms of games, I played a variety of genres, as I do every year. Did I enjoy everything equally? Absolutely not. I definitely played a couple of disappointments this year, unfortunately. I usually end up finishing games, unless I <em>really</em> don’t like them, because I want to give them a fair chance. That means I sometimes spend too much time on something that one could consider time wasted.</p>
<p>But despite that, I think it’s a good thing, too. It encourages some discipline, and it helps me appreciate the great games by having various points of comparison. And sometimes, I even end up enjoying what most people consider mediocre games.</p>
<p>So, this year has been a mixed bag, but hey, that’s been life for me for the last few years. That’s sometimes how life is, I reckon. Anyway, this post is about video games, obviously. So, let’s talk video games.</p>
<p>As always I’m here to break down my favorite picks of the year. If you’re here for the first time – this is a list of games <em>I completed</em> this year, not a list of games that released this year. Keep that in mind.</p>
<p class="previous-editions"><strong>Previous Editions</strong>
<a href="/blog/video-game-list-2024">2024</a>
<a href="/blog/video-game-list-2023">2023</a>
<a href="/blog/video-game-list-2022">2022</a>
<a href="/blog/video-game-list-2021">2021</a>
<a href="/blog/video-game-list-2020">2020</a>
<a href="/blog/video-game-list-2019">2019</a>
<a href="/blog/video-game-list-2018">2018</a>
<a href="/blog/games-i-enjoyed-most-2017">2017</a>
<a href="/blog/games-i-enjoyed-most-2016">2016</a>
<a href="/blog/games-i-enjoyed-most-2015">2015</a></p>
<p>Wow, it’s been over ten years of me doing these posts now. Time flies! It’s also been about five years since I started doing these more comprehensive write-ups, and I think that’s pretty cool.</p>
<p>Okay, enough chatter. Without further ado, here’s this year’s list:</p>
<hr />
<h2 class="ranking" data-number="1">Cyberpunk 2077</h2>
<p class="played-on">Played on PC. Released in 2020.</p>
<figure>
<img class="wide" src="/media/blog/20251231_124946_413119/source.jpg" title=""/>
</figure>
<p class="key-quote">“A happy ending? Not in this town, not for people like us.”</p>
<p>Well, what can one say about this one?</p>
<p>Starting out as V and exploring Night City in <strong>Cyberpunk 2077</strong> in its current form sort of felt like stepping into an alternate world. It’s remarkable how immersive the world feels.</p>
<p>Since I never played it before Phantom Liberty (the expansion), I never got to experience the busted version of the game that existed prior. I believe that to be a good thing.</p>
<p>Choosing Cyberpunk as my #1 pick was literally the easiest thing I’ve done in years. It’s not even close. My experience with this game was remarkable. I think it’s one of the best games I’ve ever played.</p>
<p>Night City is as much of a main character as the protagonist, since this huge metropolis is the center stage of the game’s unique setting.</p>
<p class="side right" data-title="Johnny Silverhand">After the first act of the game, Johnny Silverhand (played by Keanu Reeves) plays a significant role in this story. Both Reeves and various other actors give phenomenal performances for an incredibly cinematic experience.</p>
<p>The year is 2077. Similar to the setting of Deus Ex, augmentations and neural interfaces are pretty common, and our protagonist, V, is looking to make it big in the city. His first task? Pull a heist, and steal some experimental technology from one of the biggest corporations in town: Arasaka.</p>
<p>As it turns out, the tech they are trying to steal happens to be capable of holding a sort of synthetic copy of one’s consciousness within, and a mishap causes the tech to bleed into V’s life… and that has certain consequences.</p>
<p>I won’t say more; the game’s main story is incredible. And that’s just the main story. There’s also a lot of excellent side content, and then there’s <em>Phantom Liberty</em>, which adds a whole new section to the game, elevating it beyond its already incredible scope by even impacting the original ending of the game.</p>
<p class="side left" data-title="V's lifepath">V is the protagonist of the game. You can choose to play as a male or as a female protagonist. As you play one of the opening chapters of this game, you begin V’s story as a nomad, streetkid or corpo. This immediately sets the tone, as choosing a lifepath provides a nice starting point.</p>
<p>After the expansion released and the many rounds of bugfixing were done, the game was in a much different place compared to launch.</p>
<p>I certainly expect to play through the game again at some point, hopefully this time as a female protagonist. But then again, I sometimes say that I will replay a game and never get around to it.</p>
<p>Perhaps I’ll revisit this before the developer releases next Cyberpunk game, which I am very interested in after this particular experience.</p>
<p class="side right" data-title="Technical disaster">When this game released, it was a technical disaster. It was so bad that Sony refunded people for the PS4 version of the game in what was (at the time) an unprecedented turn of events. Thankfully, CD Projekt did continue to work on the game, and with Phantom Liberty one could argue we have something resembling the game that was originally promised to us, many years ago.</p>
<p>Cyberpunk 2077 is not for the faint of heart, however, because no one… and I do mean no one… gets a “good” ending in the Cyberpunk setting. And you know what? That makes the tone of the game quite unique.</p>
<p>It’s gritty and dark at times, but also quite beautiful.</p>
<p>There is simply no way that any of the other games I played this year come close to how immersed I was when I was exploring Night City as V, and that’s a good thing.</p>
<hr />
<h2 class="ranking" data-number="2">Final Fantasy XIV: Endwalker</h2>
<p class="played-on">Played on PC. Released in 2021.</p>
<figure>
<img class="wide" src="/media/blog/20251231_130118_438978/source.jpg" title=""/>
</figure>
<p class="key-quote">“No more shall man have wings to bear him to paradise.<br/>Henceforth, he shall walk.”</p>
<p>When you’ve spent as many hours playing a single video game as I have with Final Fantasy XIV, you’re sort of along for the ride if you’re a couple of expansions deep.</p>
<p>I’ve spent hundreds of hours roaming the world of Eorzea. When I started the <em>Heavensward</em> expansion a couple of years ago it was also a nice form of therapy for me, as it kept me busy when life had me down for a while as I mourned the loss of a long relationship. It was my escape during turbulent times. It was great.</p>
<p>But I did not want to burn myself out on the game, so I slowly made my way through the content of the main story, and got through the various expansions.</p>
<p>So, Final Fantasy XIV has been a personal favorite of mine for years now, but I had not finished the main story of the so-called “first story arc” yet. That one had come to a conclusion in 2021, with the release of the aptly-named <strong>Endwalker</strong>.</p>
<p class="side left" data-title="FFXIV as therapy">I’ve heard of more cases where people who felt unhappy about their lives found some solace in Final Fantasy XIV. It’s a beautiful thing. I don’t think it’s strange to say that this game has literally saved lives.</p>
<p>(In the meantime, a new expansion has already arrived, but it does start a new story, and this is probably where I stop playing the MMO for the time being.)</p>
<p>Endwalker ends the story arc that began with <em>A Realm Reborn</em>, a decade earlier. As you play through the various expansions, the threat to the realm seems to keep increasing, until it becomes readily apparent that the world is on the edge of an apocalypse of sorts.</p>
<p>As the Hero of Light (and Darkness, at this point) it’s up to you and the Scions (again) to make sure Eorzea is not devastated by this new threat. In other words, it’s the usual JRPG climax plot where the world is threatened and it’s up to you to ensure things go to plan.</p>
<p class="side right" data-title="Eight hundred hours">It took me a little over a 800 hours of gameplay to get to where I am right now, and this total game time is spread over about 7 years. That is… uh, a lot of game time.</p>
<p>As you progress through the main quest, you visit fantastical worlds and defeat legendary characters, and you will finally get answers to questions you may have had since you started playing the game all the way back in <em>A Realm Reborn</em>…</p>
<p>Most importantly, though, Endwalker ties up a ten-year long story in an MMO, which is quite remarkable. And it does it well, although it does not reach the same heights as the previous expansion, <em>Shadowbringers</em>.</p>
<p class="side left" data-title="What about Dawntrail?">The latest expansion has received some mixed reviews, which (I think) is understandable because the heroes are now starting out on a brand new adventure, in a brand new place. You have to get invested again!</p>
<p>I will eventually continue my Final Fantasy XIV adventure, but it won’t be right away. For now, I think I need a little bit of a break. There’s so many other things to play!</p>
<hr />
<h2 class="ranking" data-number="3">Dispatch</h2>
<p class="played-on">Played on PC. Released in 2025.</p>
<figure>
<img class="wide" src="/media/blog/20251231_160451_010376/source.jpg" title=""/>
</figure>
<p><strong>Dispatch</strong> certainly took the internet by storm this year, and for a good reason. It’s a damn good game. It was also signal-boosted by various well-known internet personalities, which may have helped, too.</p>
<p>This episodic game was created by veterans from Telltale Games, who formed AdHoc Studio. They created a remarkable episodic superhero workplace comedy game that feels like a TV show, but also has some fun gameplay.</p>
<p>In this game, you follow Robert, a superhero dispatcher. He used to be Mecha Man, a superhero himself, but his suit was destroyed. Now he’s been recruited by another super hero to help out with the dispatching of heroes… or so it seems.</p>
<p>It’s actually up to him to lead a team of reformed supervillains and get them working together to deal with all sorts of issues. That is, obviously, easier said than done…</p>
<p>Dispatch features the production quality of those old Telltale games, but it also features incredible voice talent, including the likes of Aaron Paul, of <em>Breaking Bad</em> fame and the Critical Role folks. Quality stuff, really.</p>
<p class="side left" data-title="Update cadence">I have fond memories of the various Telltale Games, which would also have episodic updates. Generally, though, it would take about 1-3 months between episodes. Having weekly updates was a lot smarter.</p>
<p>The gameplay in Dispatch mainly consists of some interactive video segments, and also a fun dispatching mini game. The latter is where this “game” shines. (It’s definitely a game, but expect something more like an interactive Netflix show.)</p>
<p>The game was updated with new episodes on a weekly basis, making for an engaging week-by-week cycle of speculation and anticipation as new episodes were added. It was a lot of fun to be part of this cycle.</p>
<p>Unlike most of my top picks, this one is actually a fairly short experience, but it was so memorable and fun that I figured I’d put it as my #3.</p>
<p class="side right" data-title="Replay value">Decisions in this game carry over to subsequent episodes, which means there’s at least enough replay value to play through the game again while making different decisions.</p>
<p>That’s about all I have to say about it. You should probably go play it, it’s not expensive and it’s also damn hilarious. (It’s also not for kids, keep that in mind. There’s a lot of colorful… language use present all throughout.)</p>
<hr />
<h2 class="ranking" data-number="4">Stellar Blade</h2>
<p class="played-on">Played on PS5. Released in 2024.</p>
<figure>
<img class="wide" src="/media/blog/20251231_130334_187118/source.jpg" title=""/>
</figure>
<p><strong>Stellar Blade</strong> is heavily inspired by NieR Automata, that much is certain. It ran well technically, played well, had good environment design, good boss fights… just a shame it was a bit lacking in the story department.</p>
<p>The plot is relatively simple: you play as EVE, a humanoid-looking robot dispatched to deal with monster threats (“naytibas”) on what remains of Earth.</p>
<p>After you arrive on Earth, EVE and her robot allies are easily dispatched by one such “alpha naytiba”… and thankfully, you are rescued by a man named Adam, who takes you to on of the last cities of humanity.</p>
<p>After this, you’re tasked to investigate various alpha naytiba threats, which are very strong creatures who threaten the safety of the city and what remains of humanity.</p>
<p>In terms of story it feels vastly inferior to the likes of NieR Automata – which was also marketed heavily via the “sexy android lady” angle, which honestly did this game a bit of a disservice, I reckon. I think it might be easy to dismiss this game for its blatant sexual marketing and miss a great hack-and-slash game.</p>
<p class="side left" data-title="Lots of outfits for EVE">Games tend to have lots of collectables; this game has unique unlockable outfits as rewards. It’s actually pretty cool. I suppose it is also part of the “sexy marketing”, but they also could have just sold the costumes as DLC. Good decisions!</p>
<p>No, fighting the big bad monsters is fun (and a bit difficult too, as you get the hang of the combat system). And I had a lot of fun. Not all games need a deep plot. Hell, Dark Souls games don’t have a plot either; and they are lauded for their deep lore.</p>
<p>Like the Souls games, you will be able to learn more about the story by reading journal entries, item descriptions etc. but the cinematic story left me feeling a tad disappointed as I thought it was a bit underdeveloped.</p>
<p class="side right" data-title="On the nose">It’s not a coincidence that the protagonist is called EVE and her companion is called Adam. There’s also some other bits in this game that are a bit on the nose. In particular the name of the monsters, “naytiba”, is a bastardization of “native”, which may be a hint to their true identity…</p>
<p>But the good news is that the game director has acknowledged this criticism and it’s likely something they will address in the already-announced sequel. If you want to try this, there’s a free demo available that you can try. (It convinced me that this game has the required depth in the combat system that I like in action games.)</p>
<hr />
<h2 class="ranking" data-number="5">Tokyo Xanadu eX+</h2>
<p class="played-on">Played on PC. Released in 2017.</p>
<figure>
<img class="wide" src="/media/blog/20251231_131307_525918/source.jpg" title=""/>
</figure>
<p>If there’s ever a franchise that I’ve been wanting to play, it’s Falcom’s <em>Trails</em> series. But since starting such a series is a bit of a daunting experience, I decided I needed to play something else.</p>
<p>After some rumination, I decided that my first <em>Falcom</em> title would be this one. <em>Xanadu</em> is probably the least popular <em>Falcom</em> franchise and has only really one modern game, so I knew I couldn’t go wrong with this one.</p>
<p>Supposedly, all of the <em>Trails</em> (and <em>Ys</em>) games are better than this, so Tokyo Xanadu eX+ is an excellent baseline Falcom experience.</p>
<p>And you know what? I feel as though that’s pretty much the case. If this is the worst a modern Falcom game has to offer, it’s still pretty damn good!</p>
<hr />
<p>Look. I’ve read that this game is sometimes compared to Persona.</p>
<p><strong>Tokyo Xanadu eX+</strong> is, in fact, nothing like Persona. It’s a real-time action game set in a modern setting with a group of youngsters. There’s some strange phenomena happening, and the kids have these special powers to investigate and defeat these demon-like creatures in this parallel space…</p>
<p>Hmm. Okay, I can see why people are making the comparison. The setting, characters and plot might be enough to draw comparisons to Persona, but, let’s be frank: a Persona game this is not.</p>
<p>It’s quite different in terms of genre (this is an action game; not turn-based), but also in terms of production quality. The budget of this game was clearly far smaller.</p>
<p>Keep in mind, this was also originally a game for the PS Vita! You can tell this is the case, though, as the environments are constrained, the combat is simple, the presentation is very basic…</p>
<p>Honestly, you might think it’s rather bland and basic as you play through the introductory chapters of the game. This game was made around the same time <em>Trails’ Cold Steel</em> games were being made.</p>
<p class="side left" data-title="Nostalgia">I originally picked this one up many years ago, but I never got around to it. There were too many incredible games to play, so playing this in 2025 was actually rather nostalgic, as the game is set in a world about a decade prior, roughly ~2015 or so. So some things feel like a blast from the past.</p>
<p>The same engine was used in those games, but the game was eventually enhanced and ported to PC and console. (Some additional “side” chapters were added, as well as a new ending with a new final boss. I understand it’s a decent chunk of extra post-game content compared to the original.)</p>
<p>So, depending on your expectations going into the game – you may find that Tokyo Xanadu is a game with enough charm and a likeable cast of characters, and enjoyable (but basic) combat.</p>
<p>I enjoyed the depth of the world building, too. You can talk to many NPCs, each with their own quirks. There’s even a feature that updates character profiles of each of these characters as you talk to them more. It definitely makes the setting feel more authentic.</p>
<p class="side right" data-title="Updated localization">I should note that an updated localization was created for the Switch re-release in 2024, and you can patch the PC version to use it. Supposedly, it significantly improved the quality of the translation. I never played it without the new localization, so I can’t speak on this topic, but the story seemed properly translated.</p>
<p>If you’re going into this with great expectations, I suspect you will be disappointed. If you temper your expectations and expect a really good PS Vita era game with the usual JRPG tropes, a good cast and a modern setting… you may actually enjoy the game like I did. I enjoyed it enough to finish it.</p>
<h2>I also played…</h2>
<h3>Dragon Age: The Veilguard</h3>
<p class="played-on">Played on PC. Released in 2024.</p>
<p>I wasn’t even going to play this one. But I needed to know… what they did to Solas (who is one of my all-time favorite characters) so I decided to play this.</p>
<p>After having started the game with fairly negative first impressions, <strong>The Veilguard</strong> has certainly grown a little on me, but after finishing it earlier this year, I do think it (unfortunately) ended up being <em>bland by committee</em>.</p>
<p>While I do still believe it to be a mediocre Dragon Age game (especially in the character writing department), it certainly got better as I continued to play. I sort of did most of the side activities that were on offer, so it wasn’t that bad? In terms of gameplay, that is. It was polished, simple, and relatively enjoyable.</p>
<p>My least favorite part of the game has to do with the fact that the game is wholly disconnected from your own personal Dragon Age history. In fact, for the majority of the game it doesn’t even feel like a Dragon Age game. Why?</p>
<p>The world state, as it was called in Inquisition, barely mattered. Because no locations from previous games are visited, this sort of feels like a sort of weird spin-off from the main games with a confused tone.</p>
<p>I played this one for Solas, and they gave him a proper send-off. A shame that this great game barely qualified as a Dragon Age title.</p>
<h3>ELDEN RING NIGHTREIGN</h3>
<p class="played-on">Played on PC. Released in 2025.</p>
<p>It took me about fifteen hours or so for this faster, co-op incarnation of ELDEN RING made sense to me, and before it clicked. Once it did, I played it until completion and had a good time.</p>
<p>I now still have a DLC to finish, but my co-op buddy that I play this game with is usually quite busy as of late, so that’ll be for next year.</p>
<p>My only complaint about this game is that it feels a little bit too fast for my liking. I prefer the slower and intentional speed of the Soulslike single-player games.</p>
<h3>The Legend of Zelda: Tears of the Kingdom</h3>
<p class="played-on">Played on Nintendo Switch. Released in 2023.</p>
<p>I did not expect to enjoy my second outing after not clicking with <em>Breath of the Wild</em>, but somehow the new mechanics actually clicked and the additions to the game convinced me that it was good.</p>
<p>Enjoying this one even made me revisit the previous game (Breath of the Wild) for a bit, to see what had changed. That was pretty cool.</p>
<h3>Deponia Trilogy</h3>
<p class="played-on">Played on PC. Released in 2016.</p>
<p>Deponia is a point-and-click puzzle game. Well, it’s actually three games in one. The Deponia games do have an interesting setting and fun cast of characters, with the protagonist being the most unlikable asshole you’ve ever met.</p>
<p>The trilogy consists of three separate games that all form a greater narrative that was… a fun romp? A pretty enjoyable series, with a protagonist you will love to dislike. There’s a sequel (“Doomsday”) but I might not get to it right away.</p>
<h3>Persona 3: The Answer (DLC)</h3>
<p class="played-on">Played on PS5. Released in 2024.</p>
<p>I’m sorry to say that you can probably skip this one, unless you really want… effectively more floors all taking place in a single, repetitive dungeon.</p>
<p><em>The Answer</em> always had a bit of a reputation of being way less enjoyable than the main game because it lacked the social sim element of the base game. This remake of <em>The Answer</em> was very faithful to the original and as such, does not really come with anything besides a new dungeon and some story.</p>
<p>I does provide you a little bit of closure, so maybe it’s worth playing it for that. The dungeon could have been a little bit shorter, and I would have probably had more fun, to be honest.</p>
<h3>Warhammer 40,000: Space Marine 2</h3>
<p class="played-on">Played on PC. Released in 2024.</p>
<p>A fun romp slaying hordes of a Chaos. I’ve got little else to say about this one. It was… fine? Not really my thing.</p>
<p>I played the first one last year (while heavily medicated), and I remember it being fine then, too. Maybe I do need to play some co-op with my friends, since I didn’t actually touch the horde mode yet.</p>
<h3>The Elder Scrolls IV: Oblivion Remastered</h3>
<p class="played-on">Played on PC. Released in 2025.</p>
<p>I played a lot of this; Oblivion was one of my most played games when I was a child. However, I cannot, in good conscience, nominate this for GOTY.</p>
<p>While Oblivion has received a modern overhaul, its systems remain old and janky, and this version has major performance issues.</p>
<p>A fun nostalgic experience, though let’s be honest: there are certainly way better games. Playing this made me want to play Skyrim again, and I don’t have time for that. I do hope that they can fix the technical issues eventually.</p>
<h3>Starfield: Shattered Space (DLC)</h3>
<p class="played-on">Played on PC. Released in 2024.</p>
<p>Wow. Good thing I got this expansion for free with my copy of the game, or I would have been really, <em>really</em> disappointed.</p>
<p>While there is some cool content here, I must say that this expansion really did not deliver. We’d heard about the epic, hidden homeworld of the Va’ruun, and what we got was… less than I’d imagined.</p>
<p>A disappointment. Easily the worst first-party DLC this studio has made?</p>
<h3>Anno 1800</h3>
<p class="played-on">Played on PC. Released in 2019.</p>
<p>I did not play enough of this to give it GOTY status, but it is a really good game. I enjoyed my time settling islands and working my way through the story section. I also played one full scenario of Anno 2205, but this is the more complex game.</p>
<p>The futuristic aspect of 2205, and the gameplay depth and mechanical systems in 1800 were pretty enjoyable. I did not sample much of the DLC, and if I have some time I probably will, down the line.</p>
<p>I can see myself wasting a lot more time on this, so I should be careful!</p>
<h3>Final Fantasy XIV: The Rising Tide</h3>
<p class="played-on">Played on PS5. Released in 2024.</p>
<p>After the disappointment that was the base game, I ended up playing all of the extra content for Final Fantasy XVI and this DLC was more of the same, honestly. Although the expansion with Leviathan had a few cool moments, this extra content suffered from the same issues the base game did.</p>
<p>The first expansion just felt like filler, and with the second expansion I hated that acquiring Leviathan had no impact on the ending of the main game. In retrospect, I probably wasted my time on this, seeing as it felt like a chore.</p>
<h3>Undertale</h3>
<p class="played-on">Played on PC. Released in 2015.</p>
<p>I finally played this modern classic. Oh boy, when did this release? Time goes by too fast. I don’t need to tell you that Undertale is a good game, do I?</p>
<p>Go play it. I won’t spoil it for you.
It’s a game and an experience in one.</p>
<p><em>Heh heh.</em></p>
<h3>Pokémon: Let’s Go, Pikachu!</h3>
<p class="played-on">Played on PC. Released in 2018.</p>
<p>I got into <em>catching ’em all</em> big time. My last Pocket Monster game was <em>Moon</em>, and I never did get around to playing <em>Ultra Sun</em> on my 3DS.</p>
<p>While fun, I did not enjoy the “catch” mechanic similar to Pokemon GO. I prefer the traditional gameplay system where you fight wild Pokemon to catch them. I also played <em>Yellow Edition</em> on my 3DS, which is the first game in the series, because I figured it’d be fun to play both at the same time.</p>
<h3>Assassin’s Creed Odyssey</h3>
<p class="played-on">Played on PC. Released in 2018.</p>
<p>It took me far too long to make it through this one. I started this one in 2023 and I ended up putting the game on hold after the first few hours until 2024.</p>
<p>Then, burnout kicked my ass and I was finally able to play it at the beginning of the year. A great game, no doubt, but perhaps a bit too huge in its scope, and requires you do devote time to it if you wish to complete it.</p>
<p>A shorter <em>Assassin’s Creed</em> game is probably a better one. (I know that <em>Valhalla</em> is next chronologically, and that is suffers from this issue even more. Oh, boy.)</p>
<h3>BLUE REFLECTION &amp; SECOND LIGHT</h3>
<p class="played-on">Played on PC. Released in 2017 &amp; 2021.</p>
<p>Ah, these. They’re from Gust, the same company responsible for the Atelier games, which I have historically somewhat enjoyed.</p>
<p>I played both Blue Reflection games, and they are perfectly okay JRPGs with perhaps a little bit too much fanservice. The original game was sort of basic and quite bland, so the sequel certainly upped the production value.</p>
<p>I still think that you’re better off playing the Atelier games. They’re better than this, unless you like the school setting and the school girls, which… uh… doesn’t make for great fanservice unless you are really into weeb culture or Japanese?</p>
<p>I guess they were okay, but I notice I’m getting too old to enjoy these weeb games. I’m becoming a grumpy old man, disconnected from the youth… oh no!</p>
<h3>Professor Layton and the Azran Legacy</h3>
<p class="played-on">Played on Nintendo 3DS. Released in 2013.</p>
<p>After having the Layton games on my backlog for many, many years, I finally finished all of the games, this one being the final game in the series.</p>
<p>By the time you get to this game you’ve probably seen most of the different types of puzzles that these games offer, so I can’t say I really loved this one, but that has nothing to do with the quality of the game. As always, a Layton game remains a solid puzzle game, if that’s your cup of tea.</p>
<h3>SCARLET NEXUS</h3>
<p class="played-on">Played on PC. Released in 2021.</p>
<p>This action role-playing game released back in 2021, and I thought it had an interesting demo back then. I decided to buy the game eventually, and it’s been on my backlog ever since.</p>
<p>Unfortunately, I don’t have that many good things to say about this one after completing it: the combat is fun, but some of the sections are too repetitive. The story is often confusing, and by the time it makes sense you’re many hours into the game. (You also need to play the game twice to get the <em>full</em> story.)</p>
<p>SCARLET NEXUS also had too many anime tropes and one-dimensional characters for me to properly enjoy the party members, too. A miss for me, but I think there was some potential for this game and I know some people really seemed to enjoy it.</p>
<h2>What’s Next</h2>
<p>Now that I’m doing better and launched my business in October, I will likely have less time to play video games next year, as I will be committing more time to work and to various other personal things.</p>
<p>All I know is that I definitely will be playing <em>Metaphor: ReFantazio</em> and <em>Clair Obscur: Expedition 33</em>, the winner of many game awards this year.</p>
<p>In the meantime, I’ve finally started playing <em>The Legend of Heroes: Trails in the Sky FC</em>… and now <em>that’s</em> a retro game! (I decided that I was not going to play the remake because I wasn’t willing to wait for the second chapter to be released.)</p>
<p>I’m sure to have a write-up about that next year.</p>
<p>Besides that, I’m also really looking forward to the next <em>Fire Emblem</em> game, which was announced this year. I hope it actually releases next year and isn’t delayed, as is quite common these days.</p>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Wed, 31 Dec 2025 23:30:00 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[My 2025 Books List]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/book-list-2025" />
        <id>https://nicoverbruggen.be/260</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>Unlike last year’s list, this one is going to be a bit shorter. In terms of what I’ve read, this year has been a continuation of the last year. If you saw that list, I’m sure you can guess what’s coming next.</p>
<p>Yes, in fact, <em>all</em> of the books I have finished this year have all been books that are set in the Cosmere, by Brandon Sanderson.</p>
<p>I did start some other books. I tried! I read a bit of Douglas Adams’ <em>Life, the Universe and Everything</em> and started reading a translation of Suetonius’ <em>Lives of the Caesars</em>, but I got stuck on them on account of not really enjoying them, so I ended up dropping both.</p>
<p>Oh, and my backlog did grow a bit, too. But I wanted to commit to finishing Mistborn’s second era prior to starting anything else, and since I took a few breaks that actually took until just yesterday.</p>
<p>So, in the end it just ended up being a bunch of Sanderson books again, this year. Next year when I’m done with the last Secret Project books, I will probably start either <em>Malazan</em> or <em>Realm of the Elderlings</em>, and I’m pretty excited to read those…</p>
<p>Anyway. Without further ado, the books I finished:</p>
<h3>Cosmere</h3>
<ul>
<li>Shadows for Silence in the Forests of Hell
<ul>
<li class="series">Part of Arcanum Unbounded</li>
<li>I thought it was pretty decent, but it is probably my least favorite out of all of the short stories I’ve read so far. “Silence” is one hell of a name for a protagonist, though…</li>
</ul>
</li>
<li>Tress of the Emerald Sea
<ul>
<li class="series">Secret Project #1</li>
<li>After reading this, I can see why it is considered to be a decent starting point for new readers. That being said, I enjoyed Tress, but mostly because of how the story was told. I still think Mistborn remains the best entry point. Tress as a female lead was done well, though, and some tropes were properly subverted. A fun read!</li>
</ul>
</li>
<li>Yumi and the Nightmare Painter
<ul>
<li class="series">Secret Project #3</li>
<li>This is now my favorite Sanderson standalone. Maybe that is because I’m a sucker for a good romance story, but this one had plenty of interesting bits, Cosmere reveals, a new setting… it made for a very quick read. I finished this book in two sittings, which is very rare.</li>
</ul>
</li>
<li>The Alloy of Law
<ul>
<li class="series">Mistborn, Book 4</li>
<li>Originally intended as a standalone, but the start of a larger second era. <em>The Alloy of Law</em> can be read as a standalone book, but introduces and develops two of my new favorite all-time characters, Wax and Wayne. As I read it, I thought: “Wax is my spirit animal.”</li>
</ul>
</li>
<li>Shadows of Self
<ul>
<li class="series">Mistborn, Book 5</li>
<li>More Wax. More Wayne. A continuation of the loose threads from the previous book. Whereas <em>The Alloy of Law</em> was short and standalone, it feels very obvious that this is actually an incomplete story now. I was very happy to immediately start the next two books after this one. Also notable: the two most important female characters, Marasi and Steris, now both get excellent character development in this book.</li>
</ul>
</li>
<li>Mistborn: Secret History
<ul>
<li class="series">Part of Arcanum Unbounded</li>
<li>I revisited this tale about a scarred man’s determination to live again before starting Mistborn’s second era. Now that I’m done with Stormlight and I had read some of Era 2, I had more of an appreciation and understanding of what’d been happening in this one.</li>
</ul>
</li>
<li>The Bands of Mourning
<ul>
<li class="series">Mistborn, Book 6</li>
<li>The thing about the second era of Mistborn books is that there’s actually <em>no</em> true Mistborn left. However, the Lord Ruler (from the first trilogy) did leave behind the so-called <em>Bands of Mourning</em> which, supposedly, can bestow the wearer with incredible power… better hope the bad guys don’t get their hands on it. Sounds like a job for Wax and Wayne!</li>
</ul>
</li>
<li>The Lost Metal
<ul>
<li class="series">Mistborn, Book 7</li>
<li>If you’ve been reading other Cosmere books or short stories, you get more out of this one. <em>The Lost Metal</em> may very well be the end of Era 2, but in terms of impact and reveals when it comes to the Cosmere, it is perhaps one of the most impactful. I think in terms of impact, this one really blows the other Era 2 books out of the water. (And if you’ve read the book you know I’m not joking about how <em>explosive</em> this one is.)</li>
</ul>
</li>
</ul>
<style>
.post .content ul li > ul {
list-style: none !important;
margin: 0 !important;
padding: 0 !important;
margin-bottom: 22px !important;
}

.post .content ul li > ul li {
list-style: none !important;
margin-top: 5px;
font-size: 16px;
color: #777;
}

.post .content ul li > ul li.series {
color: var(--primary-color);
font-size: 13px;
text-transform: uppercase;
font-family: var(--font-serif);
}

.post .content ul li > ul li:before {
content: "";
}
</style>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Tue, 30 Dec 2025 13:06:59 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[Goodbye, Windows]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/goodbye-windows" />
        <id>https://nicoverbruggen.be/258</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>Let’s not bury the lede: over the last few years, I’ve seen Windows become significantly worse, and it’s been frustrating to watch, and use.</p>
<p>This is classic <em>enshittification</em> at work, and I loathe it. In fact, this is the first post in a series of blog posts about moving away from Windows, because I’ve had enough.</p>
<p>It may sound silly, but you don’t need to tolerate a shitty operating system if there’s a viable alternative.</p>
<p>As it turns out, Linux is a viable alternative for 90% of my use case, so here we are.</p>
<h3>Increasingly closed</h3>
<p>Windows 7 was celebrated as an excellent update to Windows, but its successor did not suffer the same critical reception.</p>
<p>In retrospect, I think Gabe Newell was right about Windows 8. Today, that version of Windows looks tame compared to what’s shipping now.</p>
<p>If you were there, you may recall that it was a drastic departure from what came before. The teams at Microsoft believed that they needed to adapt their UI’s for a touch-first future (the so-called “Metro UI”), a change they eventually rolled back in Windows 10.</p>
<p>But let’s go back to 2012. You see, Gabe Newell, one of the founders of Valve (you will know them from Steam), had concerns at the time about Windows closing up as a platform and Microsoft making their product worse.</p>
<p>This is what he had to say in an interview on <a href="https://allthingsd.com/20120725/valves-gabe-newell-on-the-future-of-games-wearable-computers-windows-8-and-more/">AllThingsD</a>, circa 2012:</p>
<blockquote>
<p>In order for innovation to happen, a bunch of things that aren’t happening on closed platforms need to occur. Valve wouldn’t exist today without the PC, or Epic, or Zynga, or Google. They all wouldn’t have existed without the openness of the platform.</p>
<p>There’s a strong tempation to close the platform, because they look at what they can accomplish when they limit the competitors’ access to the platform, and they say ‘That’s really exciting.’</p>
<p>We are looking at the platform and saying, ‘We’ve been a free rider, and we’ve been able to benefit from everything that went into PCs and the Internet, and we have to continue to figure out how there will be open platforms.</p>
</blockquote>
<p>In recent years, Microsoft has been changing the OS in various ways to make it more difficult to use without a Microsoft account. They added telemetry you can’t turn off, and the operating system is now getting various (potentially undesirable) AI integrations in the form of Copilot.</p>
<p>Due to these changes, you could argue that the platform has been getting more closed off in various ways as of late.</p>
<h3>A vision of the future</h3>
<p>And then there’s what the folks at Microsoft are communicating about the future of Windows (and various other products that they have or are integrating).</p>
<p>For example, David Weston, Microsoft’s current VP of Enterprise and Security, can literally be cited here:</p>
<blockquote>
<p>I truly believe the future version of Windows and other Microsoft operating systems will interact in a multimodal way. The computer will be able to see what we see, hear what we hear, and we can talk to it and ask it to do much more sophisticated things.</p>
</blockquote>
<p>There’s more in <a href="https://www.youtube.com/watch?v=ccpXNBsTaGk">this marketing video</a> which outlines a potential vision for Windows in 2030, which, unsurprisingly, is about AI.</p>
<p>Some of this even sounds useful (perhaps?), but I also think it’s hilarious to think that the folks at Microsoft are going to be able to do something like that, taking into consideration that:</p>
<ul>
<li>Windows on ARM took them over a decade to get right and still isn’t mainstream. (Apple executed the transition flawlessly.)</li>
<li>It’s been over ten years since they announced they would migrate away from the Control Panel to the Settings app. (Control Panel still exists.)</li>
</ul>
<p>(This was nicely pointed out by one of the top comments on this particular video. I just felt it was a nice thing to point out.)</p>
<p>But, anyway. I digress. Most of my serious work is done on the Mac. For years now, I’ve kept Windows around for gaming, because you can’t really play games on a Mac the same way as you can on a high-spec desktop PC with a dedicated graphics card.</p>
<h3>What’s happening on Linux?</h3>
<p>But as of late, there are fewer and fewer reasons to stick with Windows, if this is the future we’re headed towards.</p>
<p>Especially on handhelds, it seems clear that a Linux-based operating system is better at pushing out more performance for a longer period of time; i.e. gaming on Linux is more energy efficient.</p>
<p>How did we get here, you might wonder?</p>
<p>For some time now, the many talented people at Valve have been hard at work trying to get games running on Linux. Things really turned over a new leaf for gaming on Linux when the <a href="https://nicoverbruggen.be/blog/steam-deck-q1">Steam Deck</a> became a hit.</p>
<p>At that point it was becoming really clear that they were making fantastic progress in getting Windows games to run correctly on Linux.</p>
<p>And the work does not stop. After Steam Deck, next year, Valve will be releasing a small form-factor <a href="https://store.steampowered.com/sale/steammachine">Steam Machine</a> running SteamOS.</p>
<h3>How did we get here?</h3>
<p>But let’s face it. 95% of PC gaming happens on Windows. So, the simple truth though, is, that if Windows is the primary PC platform that gamers are using to play their games on, it matters seriously which direction the platform is going.</p>
<p>So, the logical question is: where <em>are</em> things going? What’s Microsoft’s strategy? To determine that, we should see what’s been happening to Windows over the past few years.</p>
<ol>
<li>
<p>Prior to the release of Windows 11, in 2015, they <a href="https://www.theverge.com/2015/5/7/8568473/windows-10-last-version-of-windows">said Windows 10 would be last version of Windows</a>, confirming that they would continue patching the OS.</p>
</li>
<li>
<p>Unsurprisingly, they changed their mind and released Windows 11 in 2021, with <a href="https://en.wikipedia.org/wiki/Features_new_to_Windows_11">minimal improvements</a> initially. They are now pushing annual feature updates.</p>
</li>
<li>
<p>Many of the significant additions to these feature updates have been mostly Copilot-related. In 2023, they introduced <a href="https://en.wikipedia.org/wiki/Microsoft_Copilot">Microsoft Copilot</a> after building upon OpenAI’s foundational large language models.</p>
</li>
<li>
<p>Their latest push in 2025 is one of “Copilot+” devices and services, and since November there’s now even more talk about an <a href="https://www.theverge.com/news/821948/microsoft-windows-11-ai-agents-taskbar-integration">agentic operating system</a>.</p>
</li>
</ol>
<p>The latest of their marketing is all about how “you can talk to Windows now”, something people have been asking for for decades!</p>
<p>Oh wait, no.</p>
<p>No one has been asking for this.</p>
<h3>My stance on LLMs</h3>
<p>When it comes to availability of applications using large language models, I think that <a href="https://chatgpt.com/">ChatGPT</a> by OpenAI is the most popular among the general population. It’s what I hear from regular people, at least.</p>
<p>I don’t think what we call “AI” is useless, but I also think that we’ve diluted the original meaning of artificial intelligence, and I also believe that we’re in a bubble.</p>
<p>Everyone is pushing AI everywhere, and I am tired of it. In particular, Microsoft is pushing it everywhere. In Windows, in Office, on GitHub, you name it.</p>
<p>Not to mention: there’s too much incorrect information being spewed out.<sup id="fnref:llms"><a class="footnote-ref" href="#fn:llms" role="doc-noteref">1</a></sup></p>
<p>In my view, there’s no real need to bundle all of this into the operating system. Copilot could have been a third-party app, and that would have been fine.</p>
<h3>Examining annoyances</h3>
<p>I strongly believe that looking at the past can help us determine whether we can trust a company, and so, in this case, Microsoft. So, let’s take a look at some of the major annoyances that are often cited in relation to Windows 11.</p>
<h4>1. Updated hardware requirements</h4>
<p>An operating system that requires particular support for various hardware, including a TPM and a modern processor, which means that certain hardware is now no longer supported without hacks. Take a look <a href="https://support.microsoft.com/en-us/windows/windows-11-system-requirements-86c11283-ea52-4782-9efd-7674389a7ba3">at the system requirements</a> here.</p>
<p>While this is not an issue for me, it is an issue for people who are still using older but totally viable computers. Those PCs are now probably better off running Linux.</p>
<h4>2. An updated but worse taskbar with crappy search</h4>
<p>Windows 11’s taskbar (which is now centered) displays weather information and still cannot find anything with the search feature consistently.</p>
<p>You cannot move the task bar like you could before, and certain drag-and-drop interactions are no longer possible now.</p>
<p>(I want to note that the fact that products like <a href="https://www.stardock.com/products/start11/">Start11</a> exist and provide a better experience than what Microsoft can ship… says enough, I think.)</p>
<h4>3. Bad updates that break stuff</h4>
<p>There’s been a bunch of bad updates hosing key functionality of the OS.</p>
<p>In particular, recent coverage related to <a href="https://www.neowin.net/news/report-microsofts-latest-windows-11-24h2-update-breaks-ssdshdds-may-corrupt-your-data/">borked storage</a>, or even <a href="https://www.theregister.com/2025/12/04/windows_11_start_explorer/">broken core parts of the OS</a>.</p>
<p>It’s just a guess, but this might have something to do with the fact that Microsoft got rid of many testers who used to do QA on Windows, or perhaps the folks at Redmond using a little bit too much AI to get their work done in a lazy way.</p>
<p>(There’s some <a href="https://www.thurrott.com/windows/windows-10/187407/microsoft-has-a-software-quality-problem">good coverage by Paul Thurrott</a> from a few years ago about how Microsoft made QA worse that likely still applies.)</p>
<h4>4. More telemetry and Microsoft Account requirement</h4>
<p>Let’s not talk about the <a href="https://learn.microsoft.com/en-us/windows/privacy/changes-to-windows-diagnostic-data-collection">worse telemetry experience</a> and required onboarding with of a Microsoft account for the latest versions of Windows 11.</p>
<p>(They recently <a href="https://www.tomshardware.com/software/windows/microsoft-clamping-down-on-windows-11-local-account-setup-latest-insider-build-removes-local-only-commands-skipping-microsoft-account-sign-in-will-crash-setup-process">removed one of the last few remaining workarounds</a>.)</p>
<p>Now, if you do a fresh install of Windows 11 today, you are basically forced to either use Windows 11 Pro and <a href="https://www.youtube.com/watch?v=KuV5jkCXsFY">follow some specific steps</a> or <a href="https://github.com/ChrisTitusTech/bypassnro">some workaround script</a> if you do not want to use a Microsoft account.</p>
<h4>5. Copilot everywhere</h4>
<p>Finally, I already mentioned it: the numerous Copilot integrations, which are difficult or impossible to completely remove.</p>
<p>Yes, you can turn it off, but it’s awful to see it integrated everywhere, even in Notepad. Why is this necessary? I don’t think it is.</p>
<h4>6. Windows is still a paid product</h4>
<p>Remind me: why would anyone want to pay for this ad-infested operating system? I know I was able to upgrade from my valid Windows 10 Pro license, but I am certainly <em>not</em> a satisfied customer lately.</p>
<p>Windows has become annoying to use, in plenty of ways that it wasn’t prior to 2020.</p>
<p>Look, it’s not like macOS is doing significantly better, but the folks at Apple aren’t actively making their operating system worse – well, maybe except for adding super rounded corners to their window chrome.</p>
<p>But if a visual refresh is the worst complaint to have about their operating system, I think we can all agree that Apple’s doing better in that department.<sup id="fnref:fn-search"><a class="footnote-ref" href="#fn:fn-search" role="doc-noteref">2</a></sup></p>
<h3>The wrong approach</h3>
<p>Some people seem content by simply sticking with an older version of Windows, or completely customizing their Windows installation.</p>
<p>In this age, the former is also a really stupid idea… you should not be running an internet-connected device with an operating system that isn’t being updated, if only for security reasons.</p>
<p>So, if you want a secure operating system, you need to remain up-to-date. That means opting into an operating system that seems to be getting progressively worse.</p>
<p>And that… is not good. I previously settled on <a href="https://nicoverbruggen.be/blog/mindows-10">tweaking my Windows install</a> to fix the problems I have with Windows, but since things seem to be getting worse, I’m not confident this is the right approach.</p>
<p>In fact, I think at this rate I’m wasting far too much time being angry or frustrated at something I use far too often, and that isn’t good.</p>
<p>I’ve learned this about life: you don’t need to tolerate bad things. Some things you can’t do anything about. But if you have options?</p>
<h3>The right approach</h3>
<p>So, let’s be realistic, then. What is the fix? Clearly, Valve is offering an option that may get us 90% of the way there with Proton and their Linux distribution, <a href="https://store.steampowered.com/steamos">SteamOS</a>.</p>
<p>Last year, I wrote about <a href="https://nicoverbruggen.be/blog/desktop-linux">using Fedora</a> in my blog post on using Linux on my mini PC. It was a fun experiment and definitely made me realize there’s some viability here.</p>
<p>I’ve known about <a href="https://bazzite.gg/">Bazzite</a> for some time now, and I figured it was time to give it a decent shot. Bazzite is a Linux distribution based on Fedora.</p>
<p>If you’re unware of how these things work, let me explain. Bazzite uses <code>rpm-ostree</code> and OCI containers as the base for the operating system, so it’s actually a fixed system that you can’t (easily) break. The OS is effectively immutable.<sup id="fnref:apple-readonly"><a class="footnote-ref" href="#fn:apple-readonly" role="doc-noteref">3</a></sup></p>
<p>Using immutable images is great for anyone who does not want to be on the receiving end of an update that breaks their system. In the event something does go wrong, you can also roll back the system partition to a previous revision after an update if something stops working, which is very nice.</p>
<p>Applications or programs that you install are likely going to be Flatpak of AppImage versions, but it is possible to install (“layer”) packages on top of the image by using <code>rpm-ostree</code>. Layering is generally considered to be a last resort, in case Flatpaks or AppImage programs are not an option.</p>
<h3>To be continued…</h3>
<p>In September, I made the decision to do a trial run of running Linux on my secondary desktop PC (with an AMD GPU<sup id="fnref:gpu"><a class="footnote-ref" href="#fn:gpu" role="doc-noteref">4</a></sup>), which is exclusively composed of older PC parts from my main computer.</p>
<p>A little over a month later, I decided to make Bazzite my main operating system on my primary computer, too, and I’ve been running it ever since.</p>
<p>Unsurprisingly, it’s been a great experience, so far.</p>
<p><em>To be continued! I’ve been daily driving Bazzite for a couple of months now, and I will have a dedicated blog post that continues where this one ends up at the start of the next year.</em></p>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:llms" role="doc-endnote"><p>Remember, LLMs are no more than glorified prediction engines for text… with a proven track record for serious inaccuracy. Even if they are 80% correct, that means that every 1 in 5 queries are incorrect. At even 99% accuracy, with millions of users, there will be issues. In such a case, there will be many people who (when using this technology) will be getting incorrect, inaccurate or just straight up wrong information.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:llms" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:fn-search" role="doc-endnote"><p>If you don’t believe me, just use the search feature on both operating systems for a couple of search queries for files you want to locate. I’ll wait. It’ll be obvious. As an aside: if you are about as outraged about how shitty file search is on Windows, I highly recommend <a href="https://www.voidtools.com/support/everything/">Everything</a> which is actually really good.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:fn-search" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:apple-readonly" role="doc-endnote"><p>Recent versions of macOS work in a very similar way. The core files from the operating system are located on a read-only volume. You do have the option to disable this, but you need to really follow some specific instructions and it’s generally not recommended anyway.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:apple-readonly" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:gpu" role="doc-endnote"><p>Since I have an AMD GPU in my system, I was able to switch easily and it was a breeze. If you’re using an NVIDIA GPU… well, the situation is definitely getting better… but you may find the experience a little bit disappointing, since you will not be getting equivalent performance out of your GPU compared to Windows.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:gpu" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sun, 21 Dec 2025 18:00:00 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[PHP Monitor in 2025 and beyond]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/phpmon-2025-and-beyond" />
        <id>https://nicoverbruggen.be/257</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p class="headline">For the last few months, I’ve been busy adding various new features and improvements to PHP Monitor, and there’s a monthly release schedule associated with the app again.</p>
<p>As we’re getting closer to the end of November, it’s that time of the year that it’s necessary to be excited about the upcoming release of PHP, and that usually means I have to push out the obligatory build of PHP Monitor with “support for PHP 8.5”.</p>
<p>I have a little bit more time to work on PHP Monitor now, as I’m currently also in this interesting phase where I’m working part-time again (this time for my own <a href="https://mylonia.com">company</a>) while simultaneously wanting to also do some projects for myself.</p>
<p>In an ideal world, I could make maintaining PHP Monitor and bringing improvements to the app my job, but sadly that isn’t possible right now. Despite that, I did make a few decisions in regard to the app.</p>
<p>The goal has always been to keep the app open source, despite a few recommendations a couple of years ago to turn PHP Monitor into a paid product.</p>
<p>Every month, I want to be able to keep improving the app, modernizing the code (migrating away from older frameworks and approaches) and I suspect that sometimes this will result in updates without major additions.</p>
<p>Let’s go over what’s been happening for the last few months.</p>
<h2>PHP Monitor 25.06: Visual Tweaks</h2>
<p>You can read more about what was introduced in June’s release by reading <a href="/blog/phpmon-25.06-available">my last blog post</a>.</p>
<p>This is the first release that adopted the new versioning system. PHP Monitor 7.2 was the last release prior to the new versioning system, and it only received two minor version updates since 7.0 due to burnout and my health issues I was dealing with at the time.</p>
<p>In this release, various icons were added to the menu items to bring the app in line with the upcoming version of macOS, Tahoe. A new icon was also added.</p>
<p>This ended up being a mostly “visual tweak” update, but it was a welcome one.</p>
<h2>PHP Monitor 25.07: Prep Release</h2>
<p>July’s release came with a fix for <code>valet secure</code> for domains with the same path. Also, the app now displays which version of Laravel Valet is running in the menu.</p>
<p>Internal logging was improved, and a couple of longstanding bugs were fixed. It’s good to get some of this stuff sorted, but for most users this ends up being a minor update, and preparation for the next (bigger) updates.</p>
<h2>PHP Monitor 25.08: Speed Boost &amp; Fixes</h2>
<p>August’s release was more significant: it introduced various speed improvements for Homebrew operations, automatic fixes for recommendations, and a new feature that lets you upgrade Laravel Valet via the GUI.</p>
<p>First off, installing PHP versions and extensions via Homebrew 6.4 or newer now uses the <code>HOMEBREW_DOWNLOAD_CONCURRENCY=auto</code> environment variable, which should significantly speed up installations and upgrades.</p>
<p>This concurrency setting will be enabled by default for <code>brew</code> in the future, but you can benefit from it today with the flag I set manually.</p>
<p>Also new: if there are any updates available to Laravel Valet, you will now be able to see that in the menu bar. Super convenient!</p>
<p>Various improvements to PHP Doctor with automatic fixes are also nice. You can now click a single “Fix Automatically” button and fix a bunch of problems. Here’s what can be fixed with a click:</p>
<ul>
<li>Any missing taps related to PHP and/or PHP extensions can be installed automatically. These are important if the PHP Version Manager and/or PHP Extension Manager are to work correctly.</li>
<li>In case Xdebug is missing <code>xdebug.mode</code>, you can have that fixed automatically. This will ensure the menu item to switch modes will be displayed in PHP Monitor’s menu.</li>
<li>PHP Monitor helpers not included in PATH? Have it added to your <code>.zshrc</code> automatically. This only works if your chosen shell is <code>/bin/zsh</code> (the default). This will let you use the <code>. pm84</code> type helpers.</li>
</ul>
<p>This release also introduced preliminary macOS Tahoe support.</p>
<h2>PHP Monitor 25.09: Improved Update Checking</h2>
<p>In September’s update, I finally implemented a more robust “background update check” feature, which now does a few new things. First off, the app now checks for updates by contacting the PHP Monitor API.</p>
<p>The app will broadcast the following required information:</p>
<ol>
<li><strong>What version of macOS is currently being used.</strong> This is relevant because I want to prevent clients running on versions of macOS that won’t be supported for future releases to <em>not</em> get a notice about updates.</li>
<li><strong>What version and build of PHP Monitor is currently being used.</strong> This one seems rather obvious, but without knowing what version is currently being used, the app can’t determine if a newer version is available. This bit of info is also relevant to point #1 above.</li>
</ol>
<h3>What information is logged?</h3>
<p>For analytics purposes, this information is logged to the database. I also hash and salt the IP address of the requester, which can be used to prevent bad actors from doing too many requests to my server, while not impacting the privacy of users, who remain anonymous.</p>
<p><strong>Despite this careful approach, if you do not want the app to send any information from your system, you can always disable the automatic update check.</strong></p>
<p>This information sent with the update check currently gives me insights on:</p>
<ul>
<li><strong>What version(s) of PHP Monitor are in use.</strong> Since older versions don’t use this endpoint, this only applies to more recent versions of the app, of course.</li>
<li><strong>What version of macOS is most popular.</strong> This also tells me what percentage of people are still on an older version of macOS.</li>
<li><strong>How many daily users the app has.</strong> As more people upgrade to version 25.09, the daily check-in feature of the background update check will be more robust and tell me how many concurrent daily users there are, on average.</li>
</ul>
<p>In my analytics dashboard, I can then see that information. For example, let’s take a look at the current distribution of macOS users, which is relevant information when considering if I should drop support for an older version:</p>
<pre><code class="language-json">{
  &quot;update_check_ins&quot;: {
    &quot;lifetime_requests&quot;: xxxxxxx,
    &quot;daily_unique_users&quot;: {
      &quot;2025-09-30&quot;: xxxxx,
      &quot;2025-10-01&quot;: xxxxx,
      &quot;2025-10-02&quot;: xxxxx,
      ...
    },
    &quot;macos_distribution&quot;: {
      &quot;26.x&quot;: {
        &quot;name&quot;: &quot;macOS Tahoe&quot;,
        &quot;unique_users&quot;: xxxxx,
        &quot;percentage&quot;: &quot;49.4%&quot;
      },
      &quot;15.x&quot;: {
        &quot;name&quot;: &quot;macOS Sequoia&quot;,
        &quot;unique_users&quot;: xxxxx,
        &quot;percentage&quot;: &quot;43.9%&quot;
      },
      &quot;14.x&quot;: {
        &quot;name&quot;: &quot;macOS Sonoma&quot;,
        &quot;unique_users&quot;: xxxxx,
        &quot;percentage&quot;: &quot;3.4%&quot;
      },
      &quot;13.x&quot;: {
        &quot;name&quot;: &quot;macOS Ventura&quot;,
        &quot;unique_users&quot;: xxxxx,
        &quot;percentage&quot;: &quot;3.2%&quot;
      }
    }
  }
}
</code></pre>
<p>Since I’m sending information about which version of macOS is running, I have another nice benefit. For future releases of PHP Monitor, I can turn off update notifications for specific versions of macOS, so that you don’t get notified about updates that are incompatible with your version of macOS.</p>
<p>This release also introduced proper support for macOS Tahoe. I’m not sure I love those rounded corners, but it is what it is.</p>
<h3>Under consideration: additional opt-in analytics</h3>
<p>I plan on also adding additional anonymous analytics to the app. I will also (obviously) provide a dedicated opt-out mechanism for this, because that seems sensible given the app will keep track of what features are being used the most.</p>
<p>To be clear, this will only be “feature use counts”, the most simple kind of analytics which won’t be associated with individual users.</p>
<p>So, for example, if you switch the PHP version, an event will be logged. After a while (every 15 minutes or so), the batched events are planned to be sent to the server with a simple JSON request, like this:</p>
<pre><code class="language-json">{
  &quot;events&quot;: {
    &quot;switch_global_php&quot;: 3,
    &quot;apply_isolation&quot;: 1,
    &quot;open_domains_window&quot;: 2,
    &quot;open_php_version_manager&quot;: 1,
  }
}
</code></pre>
<p>Notice how no personal information is included: no information about what sites you are running, what PHP versions, simply how many times a particular event occurred.</p>
<p>This feature is currently planned for next year.</p>
<h2>PHP Monitor 25.10: Big One!</h2>
<p>October’s update is coming in a few days late, but I’ve got good news. This is a rather significant release! There’s also a good reason why I was able to dedicate a significant portion of my time to the app this month.</p>
<p>Last month, I also received a significant donation (a grant of over 1200 dollars!) to help me further build PHP Monitor. A very generous individual provided this sum.</p>
<p>I’m incredibly grateful for this, since it helps me cover costs for newer hardware, server costs, etc. A very nice motivational message was also included, which really made my day.</p>
<p><strong>I want to take this opportunity to thank everyone who has supported me financially over the years!</strong> If you’d like similarly keep me working on PHP Monitor and keep development going, <a href="/sponsor">you can donate here</a>.</p>
<h3>Renewing expired certificates</h3>
<p>As a user of PHP Monitor, this is the change you are most likely to notice with 25.10, and it’s a nice quality of life improvement.</p>
<p>If you’ve been using Laravel Valet for longer than a year on a given machine, you may have bumped into this particular issue: SSL certificates tend to expire. Yeah.</p>
<p>Previous versions of PHP Monitor simply checked for the existence of these certificates, but thankfully this is now different.</p>
<p>Both PHP Doctor and the Domains window will alert you to this issue. If you’ve identified this issue via PHP Doctor, pressing “Fix Automatically” will open the Domains window for you, and present an alert similar to this:</p>
<p><img src="/media/blog/20251107_150640_381977/source.png" alt="The Domains window will let you know if any certificates have expired and are due to be renewed." /></p>
<p>It used to be that you needed to unsecure and re-secure the domain to fix this. If you set up your machine and many domains… this would be a time-consuming task, especially because there’s always a few seconds of waiting between securing/unsecuring the domain.</p>
<p>Now, this process has been automated. Because Valet restarts <code>nginx</code> each time, this process will take a while if you have many domains, but I did not want to implement my own certificate renewal process. At least you don’t need to actually do it yourself anymore! :)</p>
<h3>Architectural changes</h3>
<p>Because this release is a little interesting from a technical POV, I’m going to get a little technical. For various systems in the app, <code>static</code> singletons were being used throughout the app for the last few years, because that was sort of the way to go, back in the day.</p>
<p>Some key abstractions also used this singleton pattern. One such abstraction was the <code>FileSystem</code> abstraction, which looked like this for previous version of the app:</p>
<pre><code>var FileSystem: FileSystemProtocol {
    return ActiveFileSystem.shared
}

class ActiveFileSystem {
    static var shared: FileSystemProtocol = RealFileSystem()

    public static func useTestable(_ files: [String: FakeFile]) {
        Self.shared = TestableFileSystem(files: files)
    }

    public static func useSystem() {
        Self.shared = RealFileSystem()
    }
}
</code></pre>
<p>In theory (and practice), it was a really solid solution: a shared instance could be configured as either a <code>TestableFileSystem</code> or a <code>RealFileSystem</code>, which would make it easy to test various configurations (real or fake) for various tests: unit tests, feature tests and UI tests.</p>
<p>As long as those tests would be executed serially… which is no longer the case with the introduction of <a href="https://developer.apple.com/documentation/testing">Swift Testing</a>. (And I did just migrate all of my tests to Swift Testing!)</p>
<p>Let’s cover why this was causing issues, in particular with concurrent testing. Here’s what a test <code>struct</code> with Swift Testing used to look like:</p>
<pre><code>init() throws {
	ActiveFileSystem.useTestable([
		&quot;/home/user/bin/foo&quot;: .fake(.binary),
		&quot;/home/user/docs&quot;: .fake(.symlink, &quot;/home/user/documents&quot;),
		&quot;/home/user/documents/script.sh&quot;: .fake(.text, &quot;echo 'cool';&quot;),
		&quot;/home/user/documents/nice.txt&quot;: .fake(.text, &quot;69&quot;),
		&quot;/home/user/documents/filters/filter1.txt&quot;: .fake(.text, &quot;F1&quot;),
		&quot;/home/user/documents/filters/filter2.txt&quot;: .fake(.text, &quot;F2&quot;)
	])
}

@Test func intermediate_directories_are_automatically_created() {
	#expect(FileSystem.directoryExists(&quot;/&quot;))
	#expect(FileSystem.directoryExists(&quot;/home&quot;))
	#expect(FileSystem.directoryExists(&quot;/home/user&quot;))
	#expect(FileSystem.directoryExists(&quot;/home/user/documents&quot;))
	#expect(FileSystem.directoryExists(&quot;/home/user/bin&quot;))
}

// and more tests which run at the same time
</code></pre>
<p>When you ran multiple tests that would mutate the fake (or real) <code>ActiveFileSystem.shared</code> instance from different places (i.e. adding new directories, deleting files), everything would simply break.</p>
<p>To prevent this issue from occurring, you would need a dedicated <code>FileSystem</code> instance per test method (or file, depending on whether those individual tests need to run in parallel, too).</p>
<p>This also means that you’d need to be able to provide dedicated instances of various abstractions when running unit tests.</p>
<p>Thankfully, there is a relatively simple solution for this: dependency injection. Now, a <code>Container</code> instance is injected where required, and this gives access to the various abstractions I provide in it.</p>
<pre><code>class Container {
    var shell: ShellProtocol!
    var filesystem: FileSystemProtocol!
    var command: CommandProtocol!
    // and more
}
</code></pre>
<p>Here’s what a test currently looks like, with the new <code>Container</code>-based approach. Because a dedicated container is now created for each test, there are no more issues when multiple tests run concurrently:</p>
<pre><code>var container: Container

init() throws {
	container = Container.fake(files: [
		&quot;/home/user/bin/foo&quot;: .fake(.binary),
		&quot;/home/user/docs&quot;: .fake(.symlink, &quot;/home/user/documents&quot;),
		&quot;/home/user/documents/script.sh&quot;: .fake(.text, &quot;echo 'cool';&quot;),
		&quot;/home/user/documents/nice.txt&quot;: .fake(.text, &quot;69&quot;),
		&quot;/home/user/documents/filters/filter1.txt&quot;: .fake(.text, &quot;F1&quot;),
		&quot;/home/user/documents/filters/filter2.txt&quot;: .fake(.text, &quot;F2&quot;)
	])
}

@Test func intermediate_directories_are_automatically_created() {
	#expect(container.filesystem.directoryExists(&quot;/&quot;))
	#expect(container.filesystem.directoryExists(&quot;/home&quot;))
	#expect(container.filesystem.directoryExists(&quot;/home/user&quot;))
	#expect(container.filesystem.directoryExists(&quot;/home/user/documents&quot;))
	#expect(container.filesystem.directoryExists(&quot;/home/user/bin&quot;))
}

// and more tests which run at the same time
</code></pre>
<p>There is still work to be done to get rid of all static singletons this way, but the most important ones preventing concurrent unit tests from working have all been addressed.</p>
<h3>Automatic crash reporting</h3>
<p>For the last past years, I’ve included instructions in the README file for folks to send me their crash logs when the app obviously crashes. Very kind developers have done so, and I’ve been able to fix a variety of crashes over the years.</p>
<p>I eventually included further verbose logging to the app which can be enabled by following steps I outline in the FAQ, and this helps further identify issues, as long as people make the effort to report them.</p>
<p>There’s also some obscure crashes, crashes you might not even notice. At some point you may notice that PHP Monitor has vanished from the menu bar, you start it back up, and all seems to work fine. Maybe it didn’t start up properly?</p>
<p>Maybe PHP Monitor stopped working at some point? Who knows. In certain cases, this may have been a subtle concurrency issue that took down the app, and left behind a crash file, but you may not have done anything in particular that might have caused the issue.</p>
<p>If this happens, and it has happened to me, you don’t actually care to investigate. Now, since I don’t have automatic crash reporting via Firebase or something like that, I was not getting those crash reports at all.</p>
<p>This changes with this month’s release, as I’ve now included <code>PLCrashReporter</code>, which keeps track of whether the app crashed, and then sends the text report to my own server if you choose to share the crash report… no third-party APIs required!</p>
<p>Note that the unsymbolicated crash reports are sent without PII:</p>
<pre><code class="language-txt">Incident Identifier: 7200729B-2730-44C7-AD3E-156CA76FE721
Hardware Model:      Mac16,11
Process:         PHP Monitor EAP [11594]
Path:            /Applications/PHP Monitor EAP.app/Contents/MacOS/PHP Monitor EAP
Identifier:      com.nicoverbruggen.phpmon.eap
Version:         25.10 (1681)
Code Type:       ARM-64
Parent Process:  launchd [1]

Date/Time:       2025-11-07 12:11:57 +0000
OS Version:      Mac OS X 26.1 (25B78)
Report Version:  104

Exception Type:  SIGTRAP
Exception Codes: TRAP_BRKPT at 0x1a50a2554
Crashed Thread:  5

Thread 5 Crashed:
0   libswiftCore.dylib                  0x00000001a50a2554 0x1a4f6b000 + 1275220
1   libswiftCore.dylib                  0x00000001a5110404 0x1a4f6b000 + 1725444
2   libswift_Concurrency.dylib          0x000000028c1c4994 0x28c161000 + 407956
3   libswift_Concurrency.dylib          0x000000028c1c5d88 0x28c161000 + 413064
4   libdispatch.dylib                   0x0000000191b7efc8 0x191b6b000 + 81864
5   libdispatch.dylib                   0x0000000191b7f784 0x191b6b000 + 83844
6   libsystem_pthread.dylib             0x0000000191d25e10 0x191d23000 + 11792
7   libsystem_pthread.dylib             0x0000000191d24b9c 0x191d23000 + 7068
</code></pre>
<p>If you’ve got experience with these crash reports, you can see that there’s no stack trace here. I have access to the DSYM files which will allow me to symbolicate these crashes and find out what’s causing them.</p>
<p>That is, if it’s no Swift Concurrency crashes… those tend to be a bit cryptic sometimes. With the help of Claude Code I like to think I’ve been able to address a couple of those crashes, by the way.</p>
<p>But if anything still goes wrong, I now have access to more useful crash reports that are much easier to send my way now. In future versions of PHP Monitor, I’m considering including a custom field where people can enter their email address so I can get in touch if the issue is problematic.</p>
<h2>Coming in 25.11: Support for PHP 8.5</h2>
<p>As is the case each year, I also release an update when the latest version of PHP becomes stable sometime in November.</p>
<p>This time, PHP 8.5 is releasing later this month, and it’s looking to be very interesting. I always try to be up-to-date as quickly as possible, so it’s important for my own sites and projects that I get PHP Monitor ready for this new version of PHP, too.</p>
<p>PHP Monitor 25.11 will likely be out in a week or two, which is a few days after PHP 8.5 releases officially. As part of this update, daily builds of PHP 8.6 will also become available for installation, as is always the case.</p>
<p>Depending on how much time I have for other features and improvements, I intend on doing some further polishing of the app as part of v25.11, but it’s going to depend on a couple of external factors, too.</p>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Mon, 10 Nov 2025 07:00:00 +0100</updated>
    </entry>
        <entry>
        <title><![CDATA[Hello, Mylonia!]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/hello-mylonia" />
        <id>https://nicoverbruggen.be/255</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p class="headline">Today I’m officially launching <a href="https://mylonia.com?utm_source=nvb&amp;utm_campaign=mylaunch">Mylonia</a>, my freelance software development company. That means I’m officially going solo.</p>
<p>If you’ve been looking at my site recently (or perhaps looked at some of my profiles on social media) you might have noticed that I soft-launched the company without any fanfare by adding links to the website and a contact form here and there, but I figured it was time to make a big announcement now, too.</p>
<p>I suspect most of you will who know me from <a href="https://phpmon.app?utm_source=nvb&amp;utm_campaign=mylaunch">PHP Monitor</a>: this app for macOS is actively maintained and continues to grow to this day.</p>
<p>But over the past few years while building and supporting PHP Monitor, I used to do agency work at a great but small software development agency in here in Belgium.</p>
<p>It was a good gig for a long time, but the commute was the biggest downer sometimes. After dealing with burnout last year I finally decided this summer to make a big change and start doing freelance work.</p>
<p><img src="/media/blog/20251015_103122_904811/source.png" alt="I also built a small website at mylonia.com. You can easily contact me for work via mail, social media, or via the contact form on the website." /></p>
<p>For now this is a part-time gig, limited to two days per week. But I am very excited about it, since it means I will be adopting new workflows and I’ve been very enthusiastic about setting up a new modern, self-hosted flexible foundation for my business over the last few weeks.</p>
<p>I intend on blogging more about that, so stay tuned for a more technical breakdown of how I’m leveraging existing open source software to run my business soon.</p>
<h3>Now taking clients!</h3>
<p>Now, let’s get to the point of this post. I’m now taking on client work more formally through Mylonia. My initial focus is on Belgian companies, but I am also open to international projects.</p>
<p>Working with me can be the right choice for small teams who need technical expertise in: Laravel backends, native iOS/macOS apps, or custom developer tooling. Great for those company where you can’t justify hiring multiple specialists.</p>
<p>The same principles that went into PHP Monitor and my other projects apply here, too: I will pay careful attention to quality, solve real problems, and build for the long term with maintenance in mind, while remaining pragmatic and sometimes opinionated.</p>
<p>If your team is wrestling with technical decisions, needs Laravel guidance, or wants native Apple platform work done right, let’s talk. I’m based in the Antwerp region (Flanders) but happy to work with clients across Belgium and beyond.</p>
<div class="cta-container">
<p><b>Interested in working with me?</b><br/>Visit the <a href="https://mylonia.com?utm_source=nvb&utm_campaign=mylaunch#contact">contact page</a> and get in touch with me.</p>
</div>
<h3>Open source sponsorships</h3>
<p>Oh, and of course, I will continue to contribute to open source as long as it makes financial sense.</p>
<p>If you are working for or at a company which benefits from my open source work, consider reaching out if you think I’d make a good addition to your team or if you’d like to sponsor my work.</p>
<p>I recently received a significant grant from a kind individual who reached out, and frankly, it’s a substantial donation like this that lets me keep working on open source software while also ensuring I make money at the end of the month.</p>
<p>It’s incredibly freeing and motivating to keep doing this type of work, and (if possible) this is eventually what I’d love to spend all of my time on.</p>
<div class="cta-container">
<p><b>Interested in sponsoring my open source work?</b><br/>Visit the <a href="/sponsors">sponsorship page</a> or <a href="mailto:mail@nicoverbruggen.be">get in touch</a> with me.</p>
</div>
<style>
.cta-container {display: flex; flex-direction: column; border: 2px solid var(--primary-color); border-radius: 8px; padding-bottom: 0; margin-top: 40px; }
.cta-container p { text-align: center; padding: 10px; }
.post .content figure.key {
  background-color: var(--primary-color);
  border-radius: 8px;
  margin: 25px 0;
}
</style>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Tue, 21 Oct 2025 10:00:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[Automating Font Tweaking with Python]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/kobo-font-fix" />
        <id>https://nicoverbruggen.be/254</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>Since my last post, I did some more research in regard to this kerning thing on Kobo devices, and I wrote a whole script to automate many of the changes I’ve been manually making to various fonts.</p>
<p>In particular, I wanted to automate the <a href="/blog/kerning-rabbit-hole">creation of the legacy <code>kern</code> table</a> that Kobo devices use when rendering text on firmware 4.x. That’s the same thing I blogged about <a href="/blog/kerning-rabbit-hole">last time</a>.<sup id="fnref:firmware_5"><a class="footnote-ref" href="#fn:firmware_5" role="doc-noteref">1</a></sup></p>
<h3>How we got here</h3>
<p>It makes sense: give a software developer a tedious task he spends more than a couple of hours working on, and at that point he will consider automating it.</p>
<p>Before the advent of LLMs, this was time-consuming process. Today? Not so much.</p>
<p>It still took me twelve hours to get to the final script, but that was because I had never seriously used Python. I was also trying out some of these tools for the first time, too.</p>
<p class="disclaimer split blue"><span class="emoji">👷‍♂️</span><span><b>Keep in mind that this blog post may contain outdated or incorrect information.</b> I've made corrections where necessary, and please also take a look at the bottom of the post for corrections and updates, as there have been more developments in regard to this particular project.</span></p>
<h3>The manual approach?</h3>
<p>Like I wrote in the previous post, I needed to use <a href="https://fontforge.org/en-US/">FontForge</a> to open the customized fonts I had previously prepared, one-by-one, and then export them again. For each of those fonts, I needed to make some adjustments to the name of the font family, unique ID and then export using the new “old-style kern” option.</p>
<p>I had already done this for the <code>core</code> font collection, which contained 24 font files (6 unique fonts with 4 styles each) but when I looked at the <code>extra</code> collection, I sighed.</p>
<p>Not only are there at the time of writing a grand total 67 files in that directory, but some of these fonts throw up a variety of warnings when opened with FontForge because they were made with more sophisticated font authoring tools. Oops.</p>
<p>So, manually editing them and exporting them again was not really an option. I had previously used some scripts to rename fonts, so I was wondering if it was possible to write a script to do what re-exporting with FontForge would also do.</p>
<p>Sure enough, once I consulted ChatGPT, Gemini and Claude, I got confirmation that this was, indeed, quite possible.</p>
<p>So, say hello to <strong><a href="https://github.com/nicoverbruggen/kobo-font-fix">kobo-font-fix</a></strong>.</p>
<h3>What does it do?</h3>
<p>By default, it generates Kobo-friendly fonts. For example, you can run:</p>
<pre><code class="language-bash">python3 kobofix.py *.ttf
</code></pre>
<p>What this means:</p>
<ul>
<li>Each font file is named correctly. This ensures that the correct font is used when using the <code>epub</code> renderer.</li>
<li>Each font file contains correct PANOSE information which ensures that the correct font weight and style is accessed when using the <code>kepub</code> renderer. Other metadata is also updated to make sure that the style matches the filename.</li>
<li>Each font family gets a different name, prefixed with a prefix of choice. This ensures our obligation to the OFL and the unique names policy is respected. Via a custom parameter the font name can be changed entirely.</li>
<li>For each font, kern pairs from the GPOS table are copied to the legacy <code>kern</code> table. This only applies to fonts that have a GPOS table, which is used for kerning in modern fonts, but ensures proper font rendering using the <code>kepub</code> renderer.</li>
<li>For each font, the line height is normalized to 20%, which is a great setting for most fonts.</li>
</ul>
<p>But this script does a lot more. Let’s say I found a custom font and I want to create an “NV” customized version like I have done with other fonts. In most cases, all I’ve adjusted is the line-height, but that still makes it a derivative font.</p>
<p>So, I can run:</p>
<pre><code class="language-bash">./kobofix.py --prefix NV --line-percent 20 --skip-kobo-kern *.ttf
</code></pre>
<p>This adds the “NV” prefix to the font, applies the 20% line spacing option but skips the Kobo kerning step, which may not be required.</p>
<p>If I want a custom name for this font, I can run:</p>
<pre><code class="language-bash">./kobofix.py --prefix NV --name=&quot;Cornerstone&quot; --line-percent 20 --skip-kobo-kern *.ttf
</code></pre>
<p>This is, obviously, super helpful. Assuming I don’t need to many any manual edits to a font, this makes adding normalized, new fonts to my <code>ebooks</code> repository a piece of cake. Previously this would have taken a lot of manual work, with a lot of opening fonts and exporting them…</p>
<p>If I already have a font that was previously authored, like the… I don’t know, 60+ fonts in the extra collection, all it takes is running:</p>
<pre><code class="language-bash">nico@m1ni kobo-font-fix % ./kobofix.py --prefix KF --remove-prefix NV *.ttf --line-percent 0
</code></pre>
<p>For example, I ran this on NV Elstob, which is a tweaked version of <a href="https://github.com/psb1558/Elstob-font">this beautiful font for medievalists</a>:</p>
<pre><code class="language-text">Processing: NV-Elstob-Bold.ttf
  --remove-prefix enabled: using 'Elstob' as the new family name.
  Renaming the font to: KF Elstob Bold
  PANOSE corrected: bWeight 8-&gt;8, bLetterForm 2-&gt;2
  Kerning: extracted 342467 pairs; wrote 342467 to legacy 'kern' table.
  Saved: KF_Elstob-Bold.ttf
  Skipping line adjustment step.

Processing: NV-Elstob-BoldItalic.ttf
  --remove-prefix enabled: using 'Elstob' as the new family name.
  Renaming the font to: KF Elstob Bold Italic
  PANOSE corrected: bWeight 8-&gt;8, bLetterForm 3-&gt;3
  Kerning: extracted 300746 pairs; wrote 300746 to legacy 'kern' table.
  Saved: KF_Elstob-BoldItalic.ttf
  Skipping line adjustment step.

Processing: NV-Elstob-Italic.ttf
  --remove-prefix enabled: using 'Elstob' as the new family name.
  Renaming the font to: KF Elstob Italic
  PANOSE corrected: bWeight 5-&gt;5, bLetterForm 3-&gt;3
  Kerning: extracted 286857 pairs; wrote 286856 to legacy 'kern' table.
  Saved: KF_Elstob-Italic.ttf
  Skipping line adjustment step.

Processing: NV-Elstob-Regular.ttf
  --remove-prefix enabled: using 'Elstob' as the new family name.
  Renaming the font to: KF Elstob
  PANOSE corrected: bWeight 5-&gt;5, bLetterForm 2-&gt;2
  Kerning: extracted 313998 pairs; wrote 313998 to legacy 'kern' table.
  Saved: KF_Elstob-Regular.ttf
  Skipping line adjustment step.

==================================================
Processed 4/4 fonts successfully.
</code></pre>
<p>Once this script has been executed, I can simply copy over all of the fonts to my Kobo’s <code>fonts</code> directory, eject it, and start reading.</p>
<h2>What issues did I bump into?</h2>
<h4>Python is new to me</h4>
<p>Let’s be honest: Python is not my forte. I usually build applications with PHP or Swift, so I had to get used to Python’s syntax, which thankfully didn’t take too long.</p>
<p>Still, I feel as though using indentation instead of curly brackets is proper madness!</p>
<h4>Size restriction on kern table</h4>
<p>Initially, it appeared as if it was a good idea to write all of the kern pairs to a single subtable. This was actually originally recommended by one of the LLMs for maximum compatibility.</p>
<p>Sadly, this meant that for certain fonts with many pairs, this would simply not work. For fonts with too many pairs, the table would not be read by my e-reader, resulting in the default “zero-kerning” approach you get without this tweak.</p>
<p>The secret, then, was to ensure that no more than 10k pairs are stored per sub-table:</p>
<pre><code class="language-python">@staticmethod
def add_legacy_kern(font: TTFont, kern_pairs: Dict[Tuple[str, str], int]) -&gt; int:
    &quot;&quot;&quot;
    Create or replace a legacy 'kern' table with the supplied pairs.
    Splits into multiple subtables if there are more than 10,000 pairs.
    &quot;&quot;&quot;
    if not kern_pairs:
        return 0

    kern_table = newTable(&quot;kern&quot;)
    kern_table.version = 0
    kern_table.kernTables = []

    # Max pairs per subtable
    MAX_PAIRS = 10000
    items = [(tuple(k), int(v)) for k, v in kern_pairs.items() if v]

    for i in range(0, len(items), MAX_PAIRS):
        chunk = dict(items[i:i + MAX_PAIRS])
        subtable = KernTable_format_0()
        subtable.version = 0
        subtable.length = None
        subtable.coverage = 1
        subtable.kernTable = chunk
        kern_table.kernTables.append(subtable)

    font[&quot;kern&quot;] = kern_table

    return len(items)
</code></pre>
<p><del>This fix ensures that the table (with its variable amount of subtables) is written to correctly, and we all win, because we get a better reading experience. Yay!</del></p>
<p class="label orange section">Correction</p>
<p class="label_aside">March 14, 2026</p>
<p>Actually… the paragraph above is wrong. Unfortunately, most text renderers only read the first subtable. While this was a clever workaround, it’s probably best to make a selection of the 10k most relevant kern pairs and to include only those.</p>
<p>For most use cases, it just makes the fonts unnecessarily larger. For more information about this, please see the update below.</p>
<h4>Extra arguments</h4>
<p>I also made sure to add some extra documentation and flags, which have proven very useful. You can check out this message by running:</p>
<pre><code class="language-bash">python3 kobofix.py -h
</code></pre>
<p>I have declared each of these arguments like this:</p>
<pre><code class="language-python">def main():
    &quot;&quot;&quot;Main entry point.&quot;&quot;&quot;
    parser = argparse.ArgumentParser(
        description=&quot;Process fonts for Kobo e-readers: add prefix, kern table, &quot;
                   &quot;PANOSE validation, and line adjustments.&quot;,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=&quot;&quot;&quot;
    Examples:
    For a default experience, which will prefix the font with KF, add `kern` table and adjust line-height:
    %(prog)s *.ttf

    If you want to rename the font:
    %(prog)s --prefix KF --name=&quot;Fonty&quot; --line-percent 20 *.ttf

    If you want to keep the line-height because for a given font the default was fine:
    %(prog)s --line-percent 0 *.ttf

    For improved legacy support, you can remove the GPOS table (not recommended):
    %(prog)s --prefix KF --name=&quot;Fonty&quot; --line-percent 20 --remove-gpos *.ttf

    To remove a specific prefix, like &quot;NV&quot;, before applying a new one:
    %(prog)s --prefix KF --remove-prefix=&quot;NV&quot; *.ttf
        &quot;&quot;&quot;
    )

    parser.add_argument(&quot;fonts&quot;, nargs=&quot;+&quot;, 
        help=&quot;Font files to process (*.ttf). You can use a wildcard (glob).&quot;)
    parser.add_argument(&quot;--name&quot;, type=str, 
        help=&quot;Optional new family name for all fonts. Other font metadata like copyright info is unaffected.&quot;)
    parser.add_argument(&quot;--prefix&quot;, type=str, default=DEFAULT_PREFIX, 
        help=f&quot;Prefix to add to font names. Required. (Default: {DEFAULT_PREFIX})&quot;)
    parser.add_argument(&quot;--line-percent&quot;, type=int, default=DEFAULT_LINE_PERCENT, 
        help=f&quot;Line spacing adjustment percentage. Set to 0 to make no changes to line spacing. (Default: {DEFAULT_LINE_PERCENT})&quot;)
    parser.add_argument(&quot;--skip-kobo-kern&quot;, action=&quot;store_true&quot;, 
        help=&quot;Skip the creation of the legacy 'kern' table from GPOS data.&quot;)
    parser.add_argument(&quot;--remove-gpos&quot;, action=&quot;store_true&quot;, 
        help=&quot;Remove the GPOS table after converting kerning to a 'kern' table. Does not work if `--skip-kobo-kern` is set.&quot;)
    parser.add_argument(&quot;--verbose&quot;, action=&quot;store_true&quot;, 
        help=&quot;Enable verbose output.&quot;)
    parser.add_argument(&quot;--remove-prefix&quot;, type=str,
        help=&quot;Remove a leading prefix from font names before applying the new prefix. Only works if `--name` is not used. (e.g., --remove-prefix=\&quot;NV\&quot;)&quot;)
</code></pre>
<h3>So, what was the point?</h3>
<p>I can now automate these fixes for many fonts, including the <code>extra</code> collection I was talking about earlier in this post. All I need to run is:</p>
<pre><code class="language-bash">nico@m1ni kobo-font-fix % ./kobofix.py --prefix KF --remove-prefix NV *.ttf --line-percent 0
</code></pre>
<p>In fact, you can get a copy of my “KF” (Kobo-fixed) fonts as part of the latest release of my latest release in the <a href="https://github.com/nicoverbruggen/ebook-fonts/releases">ebook-fonts</a> repository.</p>
<p>Make sure to pick up the <code>kobo-core</code> or <code>kobo-extra</code> files if you plan on reading any books purchased via the Kobo Store or converted via Calibre.<sup id="fnref:kfnv"><a class="footnote-ref" href="#fn:kfnv" role="doc-noteref">2</a></sup></p>
<p>I’ve also shared my <a href="https://github.com/nicoverbruggen/kobo-config">Kobo configuration</a>, which contains customizations to a NickelMenu config as I use it on my device.</p>
<p>If you want to fix a font that appears to be broken on Kobo devices, you can try <a href="https://github.com/nicoverbruggen/kobo-font-fix">kobo-font-fix</a>. Obviously, please keep font licenses in mind, and do not modify fonts you have no permission to modify.</p>
<p class="label section">Update</p>
<p class="label_aside">March 14, 2026</p>
<p>I am currently working on an updated version of this script that includes a couple of important fixes. In order to make it easier to fix fonts, I’ve now added a couple of presets that can be directly used, and I’ve also added a <code>--dry-run</code> setting.</p>
<p>I’ve been able to verify that this script is not necessary if you set <code>webkitTextRendering</code> to <code>optimizeLegibility</code>, because then the renderer will correctly read GPOS data, or so it seems. This way you won’t need the legacy <code>kern</code> table.</p>
<p>The vast majority of users will unfortunately not get proper kerning and ligatures, because this workaround is not the default behaviour, so I still think there’s a good reason to keep the script around for the foreseeable future.</p>
<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:firmware_5" role="doc-endnote"><p>Currently, Rakuten is testing firmware 5.x which will likely come to these newer Kobo devices at some point. This updated version is supposedly using a newer version of Qt, so perhaps their font rendering code will also be updated. It’s my understanding that some webview (likely QtWebEngine?) is involved. I should note that I have yet to try this updated version, but I figure this script is going to be useful regardless since it is unlikely all Kobo devices will receive this latest update. In fact, I suspect only the latest devices at the time of writing (Clara BW, Clara Color, Libra Color) will receive this update. Of course, I could be wrong, but that is my suspicion.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:firmware_5" role="doc-backlink">↩</a></p></li>
<li class="footnote" id="fn:kfnv" role="doc-endnote"><p>Each of these fonts is either a KF or an NV version, and both versions can coexist on any device, although compatibility may vary depending on the device in question. I recommend using the KF fonts on Kobo devices only.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:kfnv" role="doc-backlink">↩</a></p></li></ol></div>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sun, 24 Aug 2025 20:05:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[Down the Kobo Font Kerning Rabbit Hole]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/kerning-rabbit-hole" />
        <id>https://nicoverbruggen.be/253</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p>Not too long ago, I released <a href="https://github.com/nicoverbruggen/ebook-fonts/releases/tag/v2025.07.19">version 2.0</a> of my collection of tweaked fonts, and I was pretty sure I was done tweaking fonts for the rest of the year.</p>
<p>However, this morning I received a message in my inbox from a friendly person who linked me to an interesting post on the MobileRead forums and noted:</p>
<blockquote>
<p>One suggestion would be to consider adding ‘old-style’ kern tables so that kerning works correctly when reading <code>kepub</code> files: see e.g. <a href="https://www.mobileread.com/forums/showthread.php?t=259102">link</a>.</p>
</blockquote>
<p>Okay, that’s cool. Kerning is notoriously bad on Kobo’s <code>kepub</code> renderer, so perhaps it was possible to re-export my fonts and improve them even further?</p>
<p>The email continued:</p>
<blockquote>
<p>I’ve had some luck in the past with getting fonts to kern correctly on Kobo by just using Fontforge and exporting them with old-style kerning tables enables.</p>
</blockquote>
<p>So, I gave it a try and exported my go-to option, Charter. Modern fonts use a so-called GPOS table. That’s the abbreviated version of <strong>glyph positioning</strong> table – and it’s a fairly modern feature.</p>
<p>The GPOS table is used to store kern information in the font, and it’s a little more flexible than the old-style <code>kern</code> table. For example, you can specify various kern classes, which lets you map certain groups of glyphs together for kerning.</p>
<p>The GPOS table is, unfortunately, not used by the version of Qt that Kobo is built upon when fonts are rendered. Oops.</p>
<p>This may change in the future, but today we need to fix this by using the old-style <code>kern</code> table, which lacks a few features available via the GPOS table(s) and has a hard cap on the maximum amount of kern pairs we can add in a single font file.</p>
<img src="/media/blog/20250817_055544_Ttj3Kp/source.png" alt="It’s hard to see that these are checkboxes on macOS, but they are. Toggling this option is the key to getting proper kerning for the kepub renderer." style="width: 470px !important; margin: auto;">
<p>Take a look at the results below, I’ve made an interactive component that toggles between both states:</p>
<div class='img-a'>
<img src="/media/blog/20250817_030308_rWgBOh/source.png" 
     onclick="document.getElementById('toggle').checked ^= 1"
>
<p><b>Bad</b>: Font exported without old-style kerning.<br/>
</p>
</div>
<div class='img-b'>
<img src="/media/blog/20250817_030320_gSOGOb/source.png" 
     onclick="document.getElementById('toggle').checked ^= 1"
<blockquote>
</blockquote>
<p><b>Good</b>: Font exported with old-style kerning.<br/>
</p>
</div>
<input type="checkbox" id="toggle" style="display: none;" />
<style>
.img-a::before,.img-b::before { content: "Click or tap on this image to switch."; position: absolute; top: 30px; width: calc(100% - 40px); margin: 0 20px; border-radius: 15px; background-color: rgba(220, 220, 220, 0.2); padding: 5px 20px; font-size: 12px; opacity: 1; border: 1px solid #EFEFEF; backdrop-filter: blur(5px); pointer-events: none; }
.img-a {display: block; position: absolute; }
.img-b {display: none; position: absolute; }
.img-a img, .img-b img { cursor: pointer; border: 2px solid #333; border-radius: 5px; margin-bottom: 0px !important; }

.img-a, .img-b { position: relative; margin-bottom: 0; max-width: 450px; margin: auto; }
.img-a p, .img-b p { display: block; margin-top: 0px; margin-bottom: 40px; left: 0; padding: 8px 15px; background-color: #333; color: #FFF; width: 100%; border-radius: 5px; }
.img-a p span, .img-b p span { font-size: 14px; font-style: italic; }
.img-a p { background-color: #f0355a } .img-a img { border-color: #f0355a; }
.img-b p { background-color: #31a37b; } .img-b img { border-color: #31a37b; }

:root:has(#toggle:checked) {
  .img-a {display: none; }
  .img-b {display: block; }
}

figure > picture { display: flex; margin: auto; justify-content: center; }

</style>
<p>It’s obvious that the version with the old-style <code>kern</code> table actually seems to render differently… correctly, even! That’s great! I tried this approach with a few others fonts, and sure enough, things seem to be better this way.</p>
<p>If you’ve taken a look at the repository, you may have noticed that I’ve chosen to keep these fonts as distinct ones from the regular collection. This is done for two reasons:</p>
<ul>
<li>I want it to be obvious which fonts use Kobo-compatible old-style kerning.</li>
<li>When using old-style kerning, fonts may not render correctly on other operating systems.</li>
</ul>
<p>In particular, FontForge warns you about Windows when exporting only with old-style kerning.</p>
<img src="/media/blog/20250817_055515_6OGPni/source.png" alt="The warning presented by FontForge when exporting. Depending on how many kern pairs were part of the font, hundreds or even thousands of kern pars may not function correctly on Windows." style="width: 470px !important; margin: auto;">
<p>There is another toggle you can use (<em>Windows-compatible ‘kern’</em>), but for some fonts this causes issues when exporting due to limitations in the kerning table.</p>
<p>As it turns out, there’s a maximum size that is reached and certain kerning info may be missing beyond a particular point.</p>
<p>If you want to have the best reading experience with your <code>kepub</code> books, using the Kobo-compatible fonts is now the way to go.</p>
<h3>Enabling ligatures for <code>kepub</code> files</h3>
<p>It is possible to tweak your Kobo device so it will render ligatures, even when using the <code>kepub</code> render. This means that certain ligatures, like <strong>ﬁ</strong> or <strong>ﬀ</strong>, will be displayed correctly.</p>
<style>
.ligatures { display: flex;  text-align: center; justify-content: center; padding: 20px; }
.ligatures span { font-size: 48px; font-family: var(--font-serif); margin-bottom: 10px; }
.ligatures section { flex-grow: 1; display: flex; flex-direction: column; font-size: 16px; color: #666; }
</style>
<div class="ligatures">
<section>
<span>fi</span>
regular
</section>
<section>
<span>ﬁ</span>
ligature
</section>
</div>
<p>To do this, you need to modify <code>.kobo/Kobo/Kobo eReader.conf</code>. You need to add an override for <code>webkitTextRendering</code>, but this WILL break justified text, so you will need to turn on left-aligned text, too.</p>
<pre><code>[Reading]
webkitTextRendering=optimizeLegibility
</code></pre>
<p>I recommend turning this on <em>unless</em> you like reading justified text. You <strong>must fully restart</strong> your device, and now ligatures should render correctly.</p>
<p class="label section">Update</p>
<p class="label_aside">March 14, 2026</p>
<p>It should also be noted that setting <code>webkitTextRendering</code> to <code>optimizeLegibility</code> will also make it so that GPOS data is read correctly! This way you won’t need the legacy <code>kern</code> table.</p>
<p>But unless you added this setting, the vast majority of users will unfortunately not get proper kerning and ligatures unless the workaround with the old-style <code>kern</code> table is used, which is usually limited to about 10k kern pairs.</p>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Sun, 17 Aug 2025 18:00:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[How Poor Sleep Quality Ruined My Life (No, Really)]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/sleep-quality" />
        <id>https://nicoverbruggen.be/251</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<style>
.post .content blockquote em { font-size: 18px !important; }

.metabox { 
   margin: 50px 0px !important;
   padding: 16px 32px !important;
   border-radius: 0px !important;
   background-color: #fff1b3 !important;
}
.metabox a {
   font-size: 14px;
}
</style>
<p>Last October, when I first sought professional help after getting my depression diagnosis, and it became clear that I was going to be unable to do my job for an extended period of time.</p>
<p>I made a quick blog post (titled <a href="https://nicoverbruggen.be/blog/burnout"><em>Burned Out</em></a>) to inform everyone that things were not going well, and that I was going to be inactive for a bit.</p>
<p>It wasn’t a long post, and I closed it like so:</p>
<blockquote>
<p>I have sought professional help to ensure I can recover as soon as possible, but I’ve been told that it’s going to get worse before it’s going to get better. […] When I feel better, I plan on writing a long post on the very topic. […]</p>
</blockquote>
<p>So, it’s time. This is that long post I promised. Before I begin my tale, however, two important things.</p>
<ol>
<li>
<p>First, should note that <u>this is a retelling of my personal medical experiences</u>. Everyone is different. I am also not a medical doctor. While I do have some takeaways at the bottom of this post, I want to make it clear that this is only intended to be <em>generally</em> informative, not specific advice.</p>
</li>
<li>
<p>Second, if the idea of reading this rather lengthy blog post seems too daunting, I don’t mind if you <a href="#content-what-you-should-take-away-from-this">skip to the bottom line</a>, which includes a few key takeaways that I want almost everyone who’s reading this to think about.</p>
</li>
</ol>
<p>Okay, with that out of the way, let’s begin this story.</p>
<h2>Introduction</h2>
<p>Over the last few months, I’ve undergone quite a medical journey, with both physical and mental factors. It has been a distressing and difficult ride, but not too long ago we finally got around to identifying what I personally believe to be the underlying medical issue.</p>
<p>This is something I’d been suffering from, over a long time, that led to my burnout with both physical and mental exhaustion. It may have also caused other issues in the past.</p>
<p>Additionally, I’ll explain what could have happened if I hadn’t been proactive and somewhat intelligent during my recovery. This last part is also pretty important and may prove to be eye-opening to you.</p>
<p>I don’t particularly mind sharing my experience, as I think I did the best things I could do, but even then, I learned many new things. I also figure that it might be useful to share my thoughts on everything that has happened.</p>
<h2>Sabbatical</h2>
<p>In February of 2024, my <a href="https://nicoverbruggen.be/blog/sabbatical">sabbatical</a> finally began. This was a three-month period I had negotiated with my boss and had carved out for personal rest. I honestly believed that it was a good idea to get some rest, as I noted:</p>
<blockquote>
<p>I have been working hard to improve my life over the last two years, after the end of a <a href="https://nicoverbruggen.be/blog/boundaries">decade long relationship</a>. For the most part, all I’ve done has worked reasonably well so far: I have found myself no longer withering away, and I like to think the worst part is now behind me.</p>
</blockquote>
<p>I lamented how I was feeling, and my personal belief was that I could use a sabbatical to figure out where I was going to go next. I had some concern about potential burn-out symptoms, too, so I figured that a sabbatical might also help with that.</p>
<p>So, I took that unpaid leave. Understandably, without a clear medical reason, this seemed like the only valid approach:</p>
<blockquote>
<p>I’ve already had to take a few unpaid days over the last couple of months for personal reasons. I don’t love that I had to do that <em>to not feel overwhelmed or exhausted</em>. A longer break will hopefully ensure I won’t need to resort to such measures again.</p>
</blockquote>
<p>At the office, one of my most capable colleagues had lobbied my direct boss for a promotion and was effectively promoted to technology lead a week or two before my sabbatical began, which I felt was a great and well-deserved development. This colleague is an excellent programmer, and I would describe as driven and intelligent. Dedicated to his craft.</p>
<p>As I personally had no aspirations to lead a team or make big technical decisions at the time, I didn’t even feel envious. While we didn’t always see eye-to-eye when it came to various technical approaches, I respect the hell out of the man’s domain knowledge. And I always respect competency.</p>
<p>This new change would mean that he’d make the final technical decisions, which would avoid various discussions between the most senior developers (mainly the most senior three people, one of which was me) when no consensus was reached quickly.</p>
<p>Frankly, since the time “wasted” by some of these discussions had been a point of discussion in the past, I felt like, upon my return, things might improve for the better, once I had found some of my energy back.</p>
<p>Upon my return, I expected things to be more streamlined and positive.</p>
<h2>Post-Sabbatical Blues</h2>
<p>My three-month sabbatical quickly came and went, as these things tend to do, and wrote <a href="https://nicoverbruggen.be/blog/thoughts-on-taking-a-break">another post</a>, where I expressed a certain uncertain sentiment:</p>
<blockquote>
<p>I must admit that while I have gotten a lot of rest over the last three months, I don’t feel in a remarkably better mood than I did when my break started. I had hoped that things would shift dramatically, but sadly the dreadful weather has worked against me and as a result my overall mood has been middling at best.</p>
</blockquote>
<p>I returned to work, but I was not as happy as I should have been after a three-month break.</p>
<p>Perhaps this would simply be a new and better phase at the company. Now, if you’ve followed me during this period, you can probably guess where this is going, because in July…</p>
<h2>Shields Down</h2>
<p>I wrote <a href="https://nicoverbruggen.be/blog/shields-down">Shields Down</a>, a blog post that informed the world that I would be searching for something new to do in 2025. I would be leaving the company I’d been employed at for the last ten years.</p>
<p>You can read that post if you want to know more about the circumstances of why I decided that it was time for a new challenge, as soon as possible.</p>
<p>I was now ready to start exploring new opportunities… is what I would have told you, but sadly, my health would soon not allow this to happen.</p>
<h2>Slow Panic</h2>
<p>Then, as the summer began, I was no longer working on key projects inside of DIVE, instead working on the smaller projects that also needed to be done. Two weeks after realizing I’d be searching for a new job, I’d started feeling a little unwell.</p>
<p>During work hours, at the office, I remember feeling nauseated all of a sudden. A distinctly familiar feeling crawled up on me: slow panic. Not a full-blown panic attack, but a slow feeling of distress was apparent, with various other bodily functions affected.</p>
<p>In particular, my stomach started twisting and turning, and I wasn’t sure what was happening. I was definitely worried about my work situation, but it had only been two weeks, and I was probably fine until at least December to figure things out.</p>
<p>When I got home, I scheduled a visit to the doctor. It was probably some sort of flu, I was told, and had to take a few days off.</p>
<p>I continued to work on my new projects, but I eventually asked my boss if I could take all of my vacation days in August, as other employees were doing the same.</p>
<p>I still wasn’t feeling well and I also wanted to take some time to do some research to look for a new job, so I asked if I could take the entire month of August off.</p>
<p>This would mean two weeks of paid leave, and two weeks unpaid. Again, in retrospect I made the stupid mistake of going for unpaid vacation time, but I figured at this point… what does another two weeks cost? If it gives me peace of mind, it might be worth it!</p>
<p>How foolish of me.</p>
<h2>Unwell</h2>
<p>More trouble began sometime in the middle of August. Some of the same symptoms I’d felt in July were popping up again. This time, more frequently.</p>
<p>I wisely decided to get basic full physical check-up, as I’d had moments of feeling unwell (spells of dizziness, an upset stomach, headaches, exhaustion, more stress than usual) and it had been rather sudden without an obvious cause.</p>
<p>No obvious cause besides the added stress of needing to find a new job, that is, but I did not believe that this was such a major contributor to feeling unwell.</p>
<p>So I got blood work done, and everything came back clean. In addition to blood work, I got some urine work done, same outcome. Stool samples, same outcome. I wanted to be sure, so I asked to get those tests done.</p>
<p>They all came back fine. I got the all clear. I was healthy. Or so it seemed.</p>
<p>I still wasn’t feeling well, and worse, digestive issues seemed to get worse. After consulting with my physician I got a referral for a gastroscopy and went to the hospital to get my stomach checked out.</p>
<p>After examination, my stomach, oesophagus and throat seemed to be fine. In fact, due to the weight loss I’d worked on for the last years, I was in better health than the last time I’d gone for a gastroscope about half a decade ago.</p>
<p>So, I was supposedly fine. Unfortunately, I <em>felt</em> anything but healthy. I <em>felt</em> horrible.</p>
<p>The neck pain and dizziness in particular were quite disorienting, and I recall there being a period in 2019 where I had similar health issues that lasted a couple of weeks. Those issues kept coming back every now and then, even during times of COVID.</p>
<p>Back then, the symptoms would eventually go away again after some time, but I had always assumed the issue had been my blood pressure at the time.</p>
<p>After a visit to the cardiologist in 2021 it was confirmed that I needed to be more active, and my blood pressure troubles would be resolved. (I did just that. If you want to read more about that, check out <a href="https://nicoverbruggen.be/blog/character-development">my blog post</a>.)</p>
<p>Regardless, I thought that by 2022 the issue was gone. Even better, I considered myself in tip top shape in 2023. This is when I was at peak physical fitness again. So to be (suddenly) suffering from dizziness <em>once again</em> was quite concerning.</p>
<p>It clearly seemed to me that these recurring issues were unrelated to my physical fitness, then… or at least unrelated to my cardiovascular health, which was now certainly better than average. No, the problem was back, and this time it meant something else was going on.</p>
<p>I just hoped things would get better… But at this point, it was nearing the end of August, and I wasn’t keen on getting back to work like this. A commute to the office while dizzy could pose a problem for myself and other people on the road. It wouldn’t be very responsible.</p>
<p>Considering whether getting in the car is a good idea wasn’t a new concept to me: I had previously stopped driving my car for a period of time due to health concerns, and found my self-confidence on the road was getting affected by my health issues again. (Things weren’t that bad yet, but I knew last time things got really bad.)</p>
<p>So, I wasn’t willing to just keep feeling bad, or even see things get worse. So, I went back to the physician and mentioned that I thought my problem could’ve returned.</p>
<p>This time, though, my doctor noted that my blood pressure was fine. I had been right, clearly: something else was going on. He said it could also be another passing illness. I was told to wait it out, and come back if things didn’t improve.</p>
<p>Unfortunately, my sleep also progressively got worse, and so did my mood as a result.</p>
<p>When it was time to go back to work (when my doctor’s note was no longer valid), I instead notified my boss that I had to make another visit to the doctor’s office and sort this out for good, because frankly, it couldn’t go on like this.</p>
<p>The morning of that particular doctor’s visit, I won’t soon forget. I almost didn’t get a wink of sleep that night prior, and by the time it was the moment to leave, I was shaking and anxious. I called someone to take me to the doctor’s office, a family member who lives close by.</p>
<h2>Did You Say “Burnout”?</h2>
<h3>Two-pronged approach</h3>
<p>And look, I sure was happy that I didn’t need to drive myself.</p>
<p>Because here I was, at my usual physician who has known me all my life, and it was obvious that I was clearly distressed.</p>
<p>As I sat down on the chair at his desk, he said his usual thing. “Tell me,” he said. “What’s wrong?” Like all physicians, he always asks his patients to explain what’s going wrong.</p>
<p>I told him that the last few days had been really bad. Sleep had been horrible, and now I was starting to feel tight in my chest again, and I felt panicked. I explained my heart rate was spiking all the time, and that I was concerned I was having serious issues, despite the results of the previous labs.</p>
<p>I’ve had a history of panic attacks and high blood pressure in the past, but those issues were related to being overweight, or so I had assumed. You see, I wasn’t overweight any more. Hadn’t been for years! I had a great BMI, and normally my blood pressure and heart rate were basically perfect at this point, thanks to my workout routine.</p>
<p>(If you want to read about how I got there with hard work and habit management, you can read <a href="https://nicoverbruggen.be/blog/character-development">my blog post here</a>. I think it’s still a great read, by the way.)</p>
<p>So, what was going on now? Clearly, as healthy as I looked, something was going wrong.</p>
<p>“Would it make sense to go to the hospital?” I asked. “No,” he replied, “I can tell you are actually hyperventilating. This is why you feel so bad right now. Don’t worry, we need to do something about that right now and then things will improve quickly.”</p>
<p>He took things very seriously and told me that I had likely pushed myself too hard, and was now dealing with serious anxiety issues.</p>
<p>My physician gave me meds to calm my central nervous system, but I was only allowed to take these for a few days. Three days, to be more precise. If that wasn’t enough, I had to return afterwards.</p>
<p>They certainly helped, and I was able to get a little bit of sleep, but after those few days, things didn’t get any better.</p>
<p>So, discouraged and still on edge, I returned to his office, and explained that I wasn’t sure what to do next. I felt very strongly that returning to work would be borderline impossible in this state, as I felt increasingly unable to even deal with the basic day-to-day tasks without panicking.</p>
<p>This had <em>never</em> happened before, and was extraordinarily frightening to me.</p>
<p>Up until a few weeks prior, there had been no sign of any issues. Yet here I was, at the physician’s, unable to function and it had all happened <em>so quickly</em>. Frightening indeed.</p>
<p>“Generally speaking,” he explained, “you have two options, and you can combine them. If you are not opposed to talk therapy, I’d recommend seeking a therapist and getting that set up. I can get you the contact details of someone who will be able to help you. Are you open to that?”</p>
<p>I remember saying: “Absolutely, whatever it takes. I’d been thinking about going to therapy at some point, so I definitely would like to try it.”</p>
<p>He then continued: “Alternatively, or in addition to talking to a therapist, we can try to see if a light dosage of anti-depressants has a positive effect. This will take a while, as it will take a month to start working, and there is an adjustment period. You can decide which way you’d like to proceed, but I can’t give you any other medication to help you, as your blood work and further tests seem to indicate there is no real other issue we can identify. You are physically fine.”</p>
<p>That, in a nutshell, was what he said. Those were not his exact words, but that was the gist of it. We discussed those both options, and I explained that I wanted to do everything I could to get better. So, I decided to go for the two-pronged approach.</p>
<p>My doctor then told me that I was going to need to take a long period away from work while I deal with this, and that I should not feel too bad because it happens to many people these days… but I should learn from the process so it doesn’t happen again.</p>
<p>On the piece of paper I had to submit to the mutuality, it said “depression” as the reason for illness. That wasn’t a word that my physician had used when we were discussing things, but I suppose that’s what was going on… I was now officially <em>clinically</em> depressed.</p>
<blockquote class="metabox">
<p><strong>What is depression?</strong></p>
<p>A mental health disorder characterised by persistently depressed mood or loss of interest in activities, causing significant impairment in daily life.</p>
<p>Possible causes include a combination of biological, psychological and social sources of distress. Increasingly, research suggests that these factors may cause changes in brain function, including altered activity of certain neural circuits in the brain.</p>
<p>The persistent feeling of sadness or loss of interest that characterises major depression can lead to a range of behavioural and physical symptoms. These may include changes in sleep, appetite, energy level, concentration, daily behaviour or self-esteem. Depression can also be associated with thoughts of suicide.</p>
<p>The mainstay of treatment is usually medication, talk therapy or a combination of the two. Increasingly, research suggests that these treatments may normalise brain changes associated with depression.</p>
<p>→ <a href="https://www.who.int/health-topics/depression">More on depression on the website of the WHO</a></p>
</blockquote>
<h3>Starting therapy</h3>
<p>I called a psychotherapist the same day I returned from the doctor’s office, and, surprisingly, I had an appointment ready for the next day in no time.</p>
<p>During a brief intake conversation on the phone I explained my situation for a few minutes and the therapist said that upon hearing my explanation, it definitely sounds like a typical case of burnout.</p>
<p>Obviously we’d need to go into further detail during our first session the next day to truly make sure that’s what’s going on.</p>
<p>I couldn’t believe I would be able to go the next day, but it was a big relief to me. Most people find it really difficult to 1) find a therapist and 2) get an appointment anytime soon, so I am incredibly grateful to my physician for the recommendation.</p>
<p>Therapy proved to be quite interesting, so let me tell you a bit more about that in a later section. (I should note that one of the initial challenges to overcome is the guilt of not being able to work, even though you’re physically capable of doing work. Or maybe not, but you’ll still feel guilty, as was my case.)</p>
<h3>Starting physiotherapy</h3>
<p>One of the other major complaints was intense pain in my shoulders, neck and the back of my skull. This is typically a stress indicator, so my physician also recommended me to visit a physiotherapist.</p>
<p>I set up my first appointment quickly and later that week got some partial relief.</p>
<p>I’d eventually end up visiting the physiotherapist every week for the first few months, to manage my shoulder and neck pain while I was recovering. There’s a little more to this, too, but I’ll tell you a bit more about that later.</p>
<h3>Starting anti-depressants</h3>
<p>Because exhaustion and poor sleep were parts of my depressive episode, I had to take mirtazapine at the end of the day, and I would easily pass out and get some sleep because of them, as they are a sedative antidepressant.</p>
<p>You need about thirty days to get used to these, and initially you can suffer from a variety of side effects. In September, using these was the most difficult, as I suffered from many side-effects. In particular, I was very hungry and very tired most of the time.</p>
<h2>Exhausted</h2>
<h3>Reading, Reading, Reading</h3>
<p>During the first few weeks of my recovery process, I spent a lot of time simply exhausted on the couch, unable to do much besides read. I was grateful to have access to <em>The Stormlight Archive</em>, as it is fair to say it really helped me get though this initial, difficult period.</p>
<p>In particular, in Stormlight, the immortal words are spoken:</p>
<blockquote class="serif">
<p>“Life before death,<br/>Strength before weakness,<br/>Journey before destination”<br/><br/>
<em>– The First Ideal of the Knights Radiant</em></p>
</blockquote>
<p>Those Words resonated with me, and I was able to escape to Roshar for a couple of weeks as I continued to work my way through these lengthy books.</p>
<p>I started with <a href="https://www.goodreads.com/book/show/7235533-the-way-of-kings">The Way of Kings</a> and finally finished <a href="https://www.goodreads.com/book/show/203578847-wind-and-truth">Wind and Truth</a> at the end of the year, and what a ride it was! It was also quite comforting to see Kaladin, deal with depression and trauma. It made the books feel very genuine as I was dealing with my own struggles.</p>
<h3>Physical and Cognitive State</h3>
<p>Let me explain how things felt, overall.</p>
<p>Physically, I felt extremely tired. I was someone who would run about 5 km every day, and obviously this was no longer possible in my current state.</p>
<p>All I could manage was spend some time on the couch, reading. Even then, I still felt quite anxious, would shiver sometimes, and I had various issues with headaches, neck aches and shoulder pain. Most of the pain was caused by stress tension.</p>
<p>Cognitively, I felt incredibly overwhelmed. I had a hard time thinking, and even when sitting on the couch, at home, I suffered from panic attacks and felt overwhelmed when multiple senses were stimulated at the same time.</p>
<p>In particular, an example that comes to mind is this: when I was reading, I could not tolerate background noise from e.g. the radio or television. I was forced to turn that off, or become extremely irritated to the point of getting angry in no time.</p>
<p>It was a bit scary, to be honest, because I knew that I felt overstimulated in a way I had never experienced before.</p>
<p>Sleep was incredibly difficult, and the likeliest cause of my burnout truly kicking off. It only improved when I started taking mirtazapine, which helped a little with my sleep, but also left me extremely groggy. (It is a sedative antidepressant, after all.)</p>
<h3>Anti-depressant Angle</h3>
<p>In the middle of October, I had the obligation to attend as best man at a wedding of a good friend. Unfortunately, I was still taking mirtazapine at the time of the wedding, so I felt quite weak and anxious on the big day.</p>
<p>Fortunately, I was able to pull of my speech without issues, but I was very lucky that I didn’t need to speech until later in the afternoon, or I might have been too sedated, still.</p>
<p>My increased appetite also led me to feel extremely weak when not eating anything, and there was a long ride in a limousine that did not include food that left me feeling absolutely miserable. When I finally got my hands on a nice baguette I think I literally stopped shivering when the food entered my stomach. I’ve never experienced such an instant body response to food before.</p>
<p>I decided there and then that, while the mirtazapine was helping my sleep, it wasn’t nearly helping enough for this to be worth it. There were too many side effects.</p>
<p>In retrospect, it became obvious that I was not really depressed but really, really exhausted due to fragmented sleep and that explains why I did not benefit that much from these. Family members did tell me that I seemed a bit less depressed while taking these, but I personally felt so sedated that I suspect that’s mostly what they noticed.</p>
<p>I did not regret my personal decision to eventually quit these drugs after discussing this with my physician. I do believe that is important to try anti-depressants, you may find that in your case they might help.</p>
<h3>Therapy Progress</h3>
<p>After two sessions in September, therapy really properly started in October. As it usually takes some time to go over your own personal history, you may find therapy takes a bit to really get going. That certainly was the case for me, too.</p>
<p>Initially, therapy focused on two fronts: first, understanding and accepting that I needed to take some time to rest and recover, and second, understanding and highlighting the emotions related to the various things that had happened over the last few years in my personal and work life.</p>
<p>I must admit that it certainly is nice to be able to discuss sensitive topics with a therapist, because they are able to help you with certain techniques. Therapists are also trained to help you identify blind spots, and I found out a few of my own.</p>
<p>Because this period was <em>extremely</em> frustrating on a personal level, being able to vent and discuss methods of dealing with the reality of being depressed and burnt out was super valuable.</p>
<p>I remember thinking that therapy might be needed when a major relationship fell apart in 2021, and yes, I’m quite positive it might have helped at the time.</p>
<p>Better late than never, though I am glad that I was able to work through my post-breakup woes on my own. (Still, there is no weakness in admitting one needs help.)</p>
<h2>Slow Progress</h2>
<p>The next three months were effectively focused on physical and mental recovery. In particular, my mental health improved as I continued to visit my therapist and physio weekly and slowly got some more energy back.</p>
<p>However, my physical health did not improve quite as much. I was still mostly exhausted, and the situation did not seem to improve much. These were winter months here, so that meant that I spent much of my time inside, and the short days did not help. Every day seemed to blend into the next.</p>
<p>There was not much to celebrate at the end of the year, and I hoped that the new year would eventually bring clarity and healing.</p>
<p>My physician insisted that it would simply just take more time, but I still struggled with sleep. Sometimes, I’d jolt awake and gasp for air. I attributed this to GERD (acid reflux) and changing my food habits seemed to help a little, but not enough.</p>
<p>During this period, I would wake up every day with a headache, and a dry throat. By the end of the year, the feeling of being overwhelmed by sensory input slowly started to improve, and I was able to spend some time playing video games again, as long as I tried not to do so for too long at a time.</p>
<p>I also spent some time trying to code, but most of my sessions only lasted for fifteen minutes. After all, it was really tricky to remain concentrated and mentally map out the things I was working on. So, coding was something I’d try again later.</p>
<p>My favorite activity to relax my body and avoid muscle strain was still reading books. I’d do so on my e-reader, because I did not want to strain my eyes too much. Because of this, I found myself increasing the font size on the device, and taking off my glasses. This gave me fewer headaches. I also worked on a <a href="https://nicoverbruggen.be/blog/tweaking-eb-garamond">tweaked font</a> and blogged about it.</p>
<p>Every month, I’d have a check-in with my physician to discuss progress. Initially, especially these first few months, I was hopeful, but also… disappointed.</p>
<p>It was obvious that my time away from work was going to need to be extended for a while longer.</p>
<h2>Walks, Calls and Coding</h2>
<p>Since I was finally doing better in January, I decided that it was time to make more time for social visits.</p>
<p>After all, I had done a really poor job of staying in touch with everyone, since I was just wasting away on the couch.</p>
<p>I used the little bit of energy I had to go to various required medical appointments, and I met up with friends to go for short walks. I also tried my best to call friends I hadn’t heard often over the last little while, and informed them about how things were going.</p>
<p>I now felt decent enough to start watching some video content and streams again, so that’s how I would now fill my evenings, if I wasn’t reading.</p>
<p>I also started playing games a little bit more frequently, but still in short sessions, and nothing that was too demanding. I’d get headaches so quickly if I tried to do anything complex.</p>
<h2>Physical Ceiling</h2>
<p>In February, as my mental health improved, I decided to try using ChatGPT and build a little calorie tracking app for iOS. Because you see, here’s what happened: I’d been using a calorie tracking app for a long time, but my subscription had lapsed and the price had increased.</p>
<p>I usually do very basic calorie logging without a database or such, so I decided it would be nice to cobble together a quick app that would let me avoid needing to pay for this expensive subscription. I haven’t really written about it, but being sick can be sort of expensive.</p>
<p>And so, <a href="https://nicoverbruggen.be/blog/calodin-one-point-oh">Calodin</a> was born. It was fun to do some programming, but I still found it wildly exhausting, so I haven’t done any further work on the app beyond the initial 1.0 release that is currently <a href="https://apps.apple.com/us/app/calodin/id6741170081">on the App Store</a>.</p>
<p>But before I launched the app, something happened that caused me quite a bit of worry.</p>
<p>I had been tinkering on the app one late night, when I was suddenly contacted by a neighbour with some bad news. Worrying news. It turns out, on a Friday evening in the beginning of February (or was it the end of January?), my dad had gotten in a serious accident and needed some assistance, so it was up to me to step up and sort some stuff out.</p>
<p>Apparently, my father had been hit by a car at a dangerous crossing when he was on his bike, late at night… when that happens, you immediately worry about the worst case scenario. From what I initially heard, he’d been taken to the hospital, but it had been a bad incident. He’d been flung though the air, and while conscious, was in quite the pain.</p>
<p>I quickly called the hospital, and they informed me he’d been seriously injured, and had broken many ribs, and that he was now undergoing further tests to assess internal organ damage. Fortunately, it turned out that his ribs had taken on most of the force of the collision, and he only had some minor bruising on the inside of one of his lungs, something that would heal in time and wasn’t too serious.</p>
<p>He remained up in the hospital for about two weeks. The doctors told me he’d been very, very lucky. Despite that, his recovery was rather swift over the next two months considering the injuries, but he’d initially needed surgery and lots of painkillers, and it was quite stressful the first night when he was being kept in the ICU while I was being briefed on the damage.</p>
<p>The whole event really stressed me out. He could have died that day. It was a quick reminder that in life, things can happen really, really quickly. I tensed up for the next three weeks, to the point that my visits to the physiotherapist had to be upped again in order to manage my neck and shoulder issues.</p>
<p>Thankfully, therapy also helped me during this difficult period. That helped ease my mind, but I can’t say that I was feeling any less exhausted in my day-to-day life (doing mostly nothing). Sure, I was doing a little bit better mentally, but unfortunately… my next visit at my physician’s wasn’t that positive.</p>
<h2>Chronic Fatigue Syndrome</h2>
<p>Okay, at this point you might be wondering: <em>what bad news did you get?</em> Well, you probably read the chapter title, so…</p>
<p>During my monthly visit with my physician this month, the words <a href="https://www.mayoclinic.org/diseases-conditions/chronic-fatigue-syndrome/symptoms-causes/syc-20360490"><em>chronic fatigue syndrome</em> </a> were uttered. This was a surprise, but my physician said that signs were pointing in that direction.</p>
<p>ME isn’t a fun diagnosis, and the symptoms certainly lined up with my symptoms. You see, there isn’t any real cure for ME/CFS, you simply “manage” the condition and try to relieve the symptoms (with therapy, energy management, and such).</p>
<p>Ultimately, it sort of feels as though when doctors diagnose you with CFS, it effectively means “we don’t precisely know what’s wrong, but there’s a bunch of things that can cause this”.</p>
<p>Still, I strongly felt that there must still be some physical explanation for all of this, and I figured that my poor sleep quality must be involved somehow.</p>
<p>The few days prior, I had been reading about sleep apnea at the time after being concerned about my Apple Watch sleep stage results, and stumbled upon a bunch of Reddit posts of concerned folks asking why they were waking up so often at night, and what that meant.</p>
<p><a href="https://www.reddit.com/r/AppleWatch/comments/15yla90/is_it_normal_to_wake_up_a_lot_during_a_night_sleep/">Here</a> and <a href="https://www.reddit.com/r/AppleWatch/comments/1ad5c6p/why_am_i_waking_up_so_much_i_dont_remember_it_i/">here</a> are such example. Some folks explained that it’s normal to wake up without remembering it often:</p>
<blockquote>
<p>I had a sleep study done once and learnt that all of us wake up many, many times a night but we don’t know it. It is normal. That’s not to say that waking up tired is normal. I’m just saying it is not abnormal to be waking up (without knowing it) many times a night. I think the technical term of this kind of waking up is “arousal”.</p>
</blockquote>
<p>Other redditors chimed in with messages encouraging a visit to the doctor, with many noting that if you woke up exhausted or tired it could indicate your sleep quality is poor:</p>
<blockquote>
<p>Mine looks like this. I was diagnosed with severe sleep apnea. I feel sleep deprived most of the time. I’m working on getting it corrected. I hope you went to see a doctor about it.</p>
</blockquote>
<blockquote>
<p>Sleep apnea is a suspect. I have a cold and can’t breath through my nose, last nights sleep looked a lot like this.</p>
</blockquote>
<p>The consensus online seemed to be that a visit to a hospital for a sleep lab would be recommended if your sleep phases look similar, and you know what? Looking at these screenshots, I recognized these results: lots of orange “awake” moments, often quickly after the start of a REM or deep sleep cycle.</p>
<p>My respiratory rate also seemed to drop below the normal range during sleep, according to my watch. A drop in respiratory rate is often associated with sleep apnea, especially if there’s a dip, followed by an increase in heart rate and an awakening.</p>
<blockquote>
<p>As an aside: a low respiratory rate with sufficient O2 levels (&gt;95%) means your heart works efficiently and this could also be an indicator of proper physical fitness, mind you. A low heart rate at night is usually an indicator of being healthy.</p>
</blockquote>
<p>And this is what I sort of saw? Now, these watches aren’t always super accurate, but Apple Watch apparently does relatively well compared to other sleep tracking devices.</p>
<p>So, I asked my doctor if it was possible that my symptoms could also be explained by sleep apnea, and he said that it was unlikely for someone of my age and weight, but certainly not impossible. I had already been prescribed some melatonin for my sleep, but I hadn’t seen any improvement in my sleep quality.</p>
<p>(Unfortunately, sleep quality is also impacted by stress, and it’s common for doctors to simply assume that it’s stress causing these issues.)</p>
<p>I’d had this gut feeling that I was on the right track when I went through the list of side effects of sleep apnea, and asked my physician for a referral. He gave me one, and I called the sleep lab department at a local hospital for the next possible appointment where I had the biggest chance of getting checked out the fastest.</p>
<p>Mind you, it was the start of the month, and I was extremely fortunate. You see, fortunate enough to get an appointment only 5 weeks later.</p>
<p>(I later found out that the next possible appointment was likely in… wait for it… October! Now I had my appointment in April. I had called at the right time as a cancellation had caused an opening to appear.)</p>
<p>This could very well be another dead end, but I had a feeling that this might at least explain my horrible sleep. In the meantime, I read up a ton on understanding sleep, sleep quality, understanding sleep apnea, and more.</p>
<p>It was very enlightening, and I certainly won’t pretend to be a doctor, but I am <em>way</em> more informed now than I ever was before.</p>
<p>I also seriously underestimated the importance of a good night’s sleep, to be honest.</p>
<p>While I waited for my sleep study, I threw myself into a new (temporary) obsession: further customizing fonts for my e-reader. I’d been working on looking for new fun fonts ever since I tweaked EB Garamond and turned it into <a href="https://nicoverbruggen.be/blog/tweaking-eb-garamond">NV Garamond</a>. That was a fun process, and it made a lot of sense as I’d been reading a ton.</p>
<p>Eventually, I even <a href="https://nicoverbruggen.be/blog/electronic-reading-adventures">wrote another blog post</a> about it. (I mean, I even created a <a href="https://nicoverbruggen.be/fonts">dedicated page</a> on my website to distribute all these customized fonts. I also put them on GitHub. I figured sharing them might be a fun thing to do! And it looks like people are slowly finding the repository, so that’s nice.)</p>
<h2>Sleep Study &amp; Report</h2>
<p>During the first week of April, a few days before my birthday, it was time for me to head to the hospital and get the sleep study done. I was excited to find out more about my sleep quality. I was, however, quite stressed for the whole thing.</p>
<p>A sleep study involves a bunch of different steps, and you basically arrive at the hospital at 6 PM, where the nurses record an electrocardiogram to determine that there are no obvious heart issues that could be causing trouble, and then you are left alone for an hour or two, before they wire you up to many, many sensors.</p>
<p>(They also connect some electrodes to your head for the EEG. Nurses told me that it’s actually really quite helpful to be bald for this sort of stuff, because removal of these conductive blobs that they put on your head can be rather tricky when you have long hair.)</p>
<p>In the end, muscle tension near your arms, legs, chest, breathing patterns, EEG, heart rate, oxygen saturation and more are monitored. An infrared camera is also used, probably to determine your sleeping position (supine, left or right) and to check overall heat distribution and temperatures during the night.</p>
<p>Before you head to bed, you inform the nurses, and then you just try to get some sleep. They need to register you in bed for at least 8 hours, but they don’t expect you to be asleep for the entire duration.</p>
<p>I asked the nurse about the waiting list and she informed me that, actually, I’d been quite lucky because at the time (in April) the next free slots were only in October, which would mean that I’d need to wait another six months or so before getting my final results.</p>
<p>I say six months because you need to wait another four weeks before you can see a pneumologist who will do additional lung tests and then discuss your sleep test with you.</p>
<p>As they set me up, some fifteen minutes later the nurse came over to visit me to tell me that it was already obvious that my breating was a bit shallow. They did some adjustments to better track my breathing, and I was told to do some more breath work when I got home, as this could also affect my sleep quality.</p>
<p>I then slept, had a poor night’s sleep and dragged myself out of bed the next morning. I got up, took a shower, and returned home, feeling worse than ever. A bed I wasn’t used to, and poor sleep with all these wires and things on your body… not exactly excellent and fun conditions for a great night’s sleep.</p>
<p>In the meantime, I examined yoga breathing exercises online and focused on getting my breathing under control, i.e. breathing “via the stomach” (the right way) as opposed to breathing “via the chest” (the wrong way). Initially this was pretty frustrating, because you feel like you’re not getting enough air in when you’re breathing the right way.</p>
<p>This incorrect breathing had probably manifested as a consequence of anxiety and nasal congestion. Mouth-breathing isn’t necessarily bad per se, but when it becomes a common occurrence, there can be an impact to the unconscious way one breathes, as was clearly the case in my situation here.</p>
<p>A week and a half after the sleep study, my report was available online, and I went through the entire report. It was a huge PDF of about 15 pages, with a conclusion at the end.</p>
<p>The conclusion notes stated:</p>
<blockquote>
<p>Increased REM latency. Reduced N3 and REM. Absence of snoring. Arousals mostly paired with obstructive hypopneas, rarely PLM.</p>
</blockquote>
<p>I had suspected sleep apnea was in play, and it turns out I was sort of right. In case you’re curious PLM means “periodic limb movement”, and that can be when you’re moving your legs during your sleep and that causes you to wake up.</p>
<p>The good news is that hypopneas are not quite as bad as apneas, in the sense that apneas are truly dangerous because your O2 saturation goes down the toilet and this has a cardiovascular impact. Hypopnea means there’s a partial obstruction, whereas with apneas your breathing stops, full stop. The latter is more dangerous than the former, but both are equally destructive in terms of <em>sleep disruption</em>.</p>
<p>With a mild case such as mine, you’re also unlikely to need any additional medical equipment to help with the condition such as the CPAP machine (or the more common, modern version, APAP, which can dynamically adjust pressure as needed).</p>
<p>I obviously consulted my friendly neighbourhood ChatGPT and various other LLMs to tell me more about sleep deprivation, sleep disruption and explained my own situation and asked for some tentative information. Obviously I was still going to visit the specialist in a few weeks, but I wanted my answers now.</p>
<p>I also had another important thought: my sleep was often bad because of my allergies and nasal congestion. Obstructive sleep apnea as a general category (and hypopneas are part of that category) can be caused by various factors, including the position of the tongue, but also by nasal issues, GERD, and more. Given that my nose was still causing me trouble on a daily basis, I decided that enough was enough.</p>
<p>If my congested nose was potentially one of the reasons I was dealing with sleep disruption, it was time to do something about it. I figured it was time to see the ENT and discuss my options.</p>
<p>You see, I’d already made an appointment some time prior, because I wanted to work on sleep and breathing at the same time. Unsurprisingly, both were connected, which I had also suspected.</p>
<p>(Making those appointments with specialists in parallel would later turn out to have been a good decision, because I was able to take a two-track approach to getting this problem resolved. It probably sped up the process of getting help by a couple of weeks, for sure.)</p>
<p>So, in the middle of April I was able to visit the ENT and get my nose checked out. I brought a report from my previous visit with an ENT a few years prior, who had recommended surgery but had explained that there was <em>no medical urgency</em>. (The issue being a deviated septum, which meant that airflow was decreased through my left nostril.)</p>
<p>The previous ENT had emphasised that recovery would be brutal, and this was right before COVID became a thing. I had decided to postpone surgery, and when COVID happened and other health concerns became more of an issue, optional nose surgery had not really been a priority. But now, things were different.</p>
<p>I explained the situation to the ENT, and the ENT decided to do a test to determine how impaired my breathing was through my left nostril. Sure enough, impaired enough to recommend surgery, especially given the current complaints. The appropriate surgery to straighten the septum is called a septoplasty (not to be confused with rhinoplasty).</p>
<p>I told the ENT that I wanted to get this surgery done as quickly as possible, so that I could recover as quickly as possible and hopefully enjoy a somewhat okay summer to continue my burnout recovery. The date of surgery was set for the 26th of May, and it would be a few frustrating weeks to recover, but the whole thing would be considered “minor surgery”.</p>
<p>In the end, I’d get a septoconchaplasty which would straighten the septum, but also do some additional work to shrink turbinates where necessary and remove any polyps when necessary.</p>
<p>The date was set, and I returned home with a feeling of dread. I wasn’t looking forward to this surgery, but I was looking forward to being able to breathe properly for the first time in my life. (You see, due to the deviated septum and allergies my left nasal passage was almost always partially if not completely obstructed.)</p>
<h2>Specialist Visits &amp; Surgery</h2>
<p>I was to visit the hospital again to get a consultation with the pneumologist in May, after needing to wait for a few weeks. In the meantime, I had been doing some more sleep tracking with my Apple Watch which kept confirming that I was waking up often during my sleep, and my nasal congestion was getting worse, too.</p>
<p>I was happy that it wouldn’t be too long before I could get surgery, but hated the idea of that first week of recovery, even though I was very positive about the eventual outcome and results.</p>
<p>So, when I went to see the pneumologist they did some basic tests to verify that the issue was not related to my lungs; they did routine lung capacity tests, and a little after that I was able to sit down with the specialist, who told me many things that I’d already personally researched, so I was happy to get a confirmation from a specialist that I’d done my due diligence. The pneumologist also said it was a good thing that surgery for my nose was planned, as that should definitely also help. “Stress management,” she said, “Is also going to help, but I suspect your nose is going to make a big difference.”</p>
<p>The ENT had told me that there was a possibility that the sleep hypopneas were not connected to the sleep disturbances, but there was no doubt that improved breathing would likely also have a positive effect on my general sleep quality, which would impact stress levels, anxiety, and this would also further affect my sleep.</p>
<p>I went home feeling hopeful that, perhaps soon, the problem would be resolved. Now it was just time to wait for surgery.</p>
<p>Fortunately, I didn’t have to wait long.</p>
<p>Some two weeks later, the day of surgery arrived, which was a Monday. I was brought to the hospital for my overnight stay. I arrived in the morning at 11, and surgery was planned for 4 in the afternoon. The waiting was honestly the most frustrating part.</p>
<p>I was taken to the prep room at 4 in the afternoon, so there was a slight delay, I think it was 4:30 when I was in the OR. Surgery wasn’t going to take long, this is usually a 30-45 minute procedure. After going through a brief checklist with the anthesiologist I was put under, and the next thing I remember is waking up at 5:30 with no ability to breathe except through my mouth.</p>
<p>I was destined to be a mouth breather for the next five days. This was the part that I’d been dreading: as part of the surgery they’d put nasal packing in and it needed to remain in until that Friday.</p>
<p>My nose hurt, I was on antibiotics and painkillers, and a little later my nose started bleeding consistently. They put a “mustache” on you to ensure that all of the blood doesn’t end up in your mouth. The bleeding was being monitored and eventually slowed down over the next twelve hours.</p>
<p>Initially, I was also super tingly because the sedatives and anesthesia still needed to completely wear off. I was sharp mentally, but my body was still a bit sluggish. Every few hours the nurses would come to replace the “mustache” and also take blood pressure, replace the IV, and then I tried to get some sleep when it got late.</p>
<p>Unfortunately, I didn’t really get a lot of sleep that night.</p>
<p>The next morning, the specialist visited and explained everything had gone well, and that I was free to go home around noon. On Friday, I’d have to return to the hospital to have the nasal packing removed, but until then I was told to simply “endure” the annoying situation as it was.</p>
<p>I went home, visited the pharmacy to get the necessary painkillers, antibiotics and also extra fabric so I could replace the “mustache” for the next 24-48 hours. I also got some nasal spray that I’d need to start using on Saturday, the day after the packing was removed.</p>
<p>The next few days weren’t fun, but I worked my way through. On Friday, in the evening, I was the last person to visit the specialist that day. It was an unpleasant few seconds as the packing was removed, but there were no real issues. “Just some minor bleeding at the back,” I was told, “but you don’t need to worry, it’ll pass.”</p>
<p>I couldn’t exactly breathe through both nostrils yet, as everything was sore and the necessary mucuous membrane needed some more time to heal, but this was infinitely better than having both nostrils completely blocked.</p>
<h2>Broken Nose Recovery</h2>
<p>At this point, when week two of my recovery started, it was the beginning of June.</p>
<p>My nose is healing, and it will take many weeks before a complete recovery. At this point, I could already tell that my nasal congestion was less of an issue.</p>
<p>As far as my recovery is concerned, things are going well. My nose will still need to heal for the next couple of weeks and I am noticing that my mental acuity has improved significantly.</p>
<p>At the time of posting, August has arrived and things are so much better. It’s truly remarkable how big of a difference being able to breathe makes, and how much better my sleep quality seems to be.</p>
<h2>Lessons Learned</h2>
<ul>
<li>Poor sleep quality led to mental health concerns (anxiety, panic attacks) and this has likely been the case for years. There seems to have been flare-ups every few years.</li>
<li>Being around dust and various other causes of allergic reactions (e.g. animals, outdoor during hayfever season) on my part probably exacerbated the issues above.</li>
<li>Besides worrying about diet and exercise (which I’ve been doing for years now) I’m integrating good sleep hygiene into my routine.</li>
<li>Improved airflow &amp; breathing technique has led to improved stress levels, which impacts sleep, and should now let me properly recover from my depression and burnout. Progress is being made.</li>
<li>I had no idea how badly the bad sleep quality had been affecting me until I truly hit rock bottom mentally. The cognitive decline was rather substantive, now that I can compare how I feel today and how I felt back then.</li>
</ul>
<h2>What you should take away from this</h2>
<ul>
<li><strong>Sleep quality is very important, as it impacts your day-to-day life: it impacts your energy levels, your ability to think, your ability to react, and more.</strong>
<ul>
<li>If you wake up and feel exhausted, have headaches and/or find yourself falling asleep during daytime activities, it may be necessary to evaluate your sleep schedule and/or sleep health.</li>
<li>You will find many people disagree on how much sleep is needed, but aiming for at least 7 hours of decent sleep is not a luxury, it is something that will profoundly impact your life.</li>
<li>Being relaxed, being able to breathe, not being randomly woken by environmental factors all play a key part to restful sleep.</li>
<li>If you find yourself emotionally under pressure, stressed out, worried or otherwise tense (sometimes literally: are you tensing up your muscles?) you will need to do something about that because your sleep quality will suffer.</li>
<li>Sleep is required if you wish to be better equipped to deal with mental stress. In particular, REM sleep is very important for proper recovery.</li>
<li>Deep sleep and/or REM sleep may be affected if you suffer from any form of sleep apnea.</li>
<li>Good sleep also has a profound psychological impact: you are far less likely to suffer from depression when well-rested.</li>
</ul>
</li>
<li><strong>Sleep apnea, either mild or severe, can be extremely disruptive to sleep and potentially life-threatening.</strong>
<ul>
<li>Obstructive sleep apnea is actually very harmful to your cardiovascular system, especially if you stop breathing for extended periods of time.</li>
<li>Mild sleep apnea can be enough to cause you to wake up often at night, which can be very disruptive. This may prevent you from getting enough REM and deep sleep, which can be quite serious and lead to fragmented sleep.</li>
</ul>
</li>
<li><strong>Try to get some good sleep tonight. Seriously.</strong>
<ul>
<li>If your sleep has been really crap, go see a physician. Don’t let things remain this way. Don’t be like me and let things get progressively worse!</li>
<li>You may not be aware of how bad the decline has been, if you are sleeping poorly. So, consider carefully if your sleep quality may have been affected.</li>
</ul>
</li>
</ul>
<h2>Closing thoughts</h2>
<p>Wow, this all really fucking sucked, and the suck isn’t entirely over yet, but I am hopeful for the future as things seem to be improving. The scary bit is that I am <u>totally in the dark about how long I’ve been impacted by this</u>.</p>
<p>I have felt more rigid and inflexible when it comes to my energy levels for at least the last decade, and I wonder: how much were my personal relationships and career choices impacted by a mentality directly impacted by poor sleep?</p>
<p>I may never have the answer, but I suspect my life may have turned out profoundly different had I been in a different state of mind, less tired and more sharp. Over the last weeks, I’ve found myself more willing to take chances, more optimistic, more willing to change my life than ever before.</p>
<p>If you’re constantly tired, your thinking is affected in various ways. In retrospect, I can definitely feel that I wasn’t doing my best work at work because I was physically and mentally held back by poor sleep.</p>
<p>I was lucky to be curious and determined to determine what was causing all of these problems, because if I had not advocated for myself in this way, I probably would still be where I was before, taking anti-depressants and going to therapy, while continuing to struggle with poor sleep and poor breathing form.</p>
<p>During therapy sessions, I was able to communicate my dissatisfaction with myself and the industry I have been working in for the last ten years. I don’t think I’m meant to be a programmer for that many hours on end, day after day, as this was also clearly having an impact on my life.</p>
<p>I’m confident the direction I’m going now is positively impacted by the decisions I’ve made over the last twelve months. Not all of those decisions have been easy, but I do believe they have been the right ones.</p>
<p>In the coming months, I suspect many things will change. However, I am excited about the future for the first time in a long while. I’m sure I will have more to share in September, but until then, stay tuned!</p>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Fri, 08 Aug 2025 21:00:00 +0200</updated>
    </entry>
        <entry>
        <title><![CDATA[PHP Monitor 25.06 now available]]></title>
        <link rel="alternate" href="https://nicoverbruggen.be/blog/phpmon-25.06-available" />
        <id>https://nicoverbruggen.be/250</id>
        <author>
            <name><![CDATA[Nico Verbruggen]]></name>
        </author>
        <summary type="html">
            <![CDATA[<p class="headline"><strong><a href="https://phpmon.app">PHP Monitor 25.06</a></strong> is now available with a few fresh visual touches and some minor bug fixes. Read more below for details.</p>
<p>While I remain on sick leave for likely the rest of the summer, I am doing better than before (I’m finally starting to get better, after getting surgery to fix serious breathing issues) and recently decided to see if I could get a little update for PHP Monitor out.</p>
<p>I took a look at the latest announcements Apple made at WWDC, and I made a few changes to PHP Monitor to prepare it for this fall’s update to macOS 26. Apple seems to be doing a large visual refresh across their OSes with Liquid Glass, so it’s definitely going to be an interesting release.</p>
<p><img src="/media/blog/20250624_090233_ZongR4/source.jpg" alt="The new icon of PHP Monitor can be seen in the Dock, and the menu menu with icons to make each action easier to identify." /></p>
<p>With macOS using a simpler strategy for their version numbering, I figured I’d follow suit with this update in preparation for the fall.</p>
<p>As such, this new release is now version 25.06, because it released in the 6th month of 2025. This will be the new version numbering system, as it began to mean increasingly little to be on version 7.2 or something like that. (Ubuntu also uses this versioning system, so you’re perhaps maybe familiar with it there?)</p>
<h3>What’s Changed</h3>
<p>So, what’s new in this release? Here’s the highlights:</p>
<ul>
<li><strong>There’s a new versioning system in use.</strong> With this release, PHP Monitor now requires macOS 13.5, so please make sure you are on the latest version of macOS. If you are running an older version of macOS, you can continue to use PHP Monitor 7.x.</li>
<li><strong>There’s a new app icon.</strong> When macOS Tahoe releases, the icon will be updated for the new look, too, but it will be very similar to the new icon with a touch of Liquid Glass.</li>
<li><strong>PHP Monitor will now display plenty of icons</strong> next to the majority of the menu items in the main menu and the context menu. This makes it easier to see where you need to click, and this was a fairly trivial change to make. This, too, is more in line with macOS 26.</li>
<li><strong>A few bugs were also squashed.</strong> The most noteworthy one in particular is related to the spinner which was no longer being displayed when you ran commands in the Domains List. This is now fixed.</li>
</ul>
<p>Let’s be honest: this is no revolutionary new update, but it does contain some nice improvements, especially for app usability.</p>
<p><img src="/media/blog/20250624_090635_QOApKF/source.jpg" alt="The new context menu for Domains also has new icons. I found myself sometimes making a mistake and mixing up “secure” and “favorite”, with the icons this is less likely to cause confusion." /></p>
<p>This release will be followed later this year with an update to fully bring the app’s design consistent with macOS Tahoe and also brings support for the stable version of PHP 8.5 when it releases.</p>
<p>As always, you can get the new release on GitHub or update via the built-in updater if you already have PHP Monitor installed.</p>
<h3>100.000+ downloads!</h3>
<p>I also found out today that PHP Monitor <strong>has been downloaded over 100K, in fact, over 110K times</strong>. That’s incredible. Thank you all for your support, and I hope you continue to find the app very useful.</p>
<p>Fun fact: If you’re ever trying to figure out how many total downloads an app has received via GitHub, don’t forget that the API endpoint pagination caps the releases by default at 30. That’s why I thought we weren’t at 100K yet previously.</p>
<p><img src="/media/blog/20250627_084515_aBcM1t/source.png" alt="Turns out there’s been more than 30 releases of PHP Monitor, and pagination caps at 30 by default. This gives me a little bit more time to properly handle pagination in my command… next time." /></p>
<p>After checking out downloads per release, it seems there’s a couple thousand of you out there still using the app in 2025. Keep on trucking! 🚚</p>
<hr />
<p><strong>As always, I encourage you to sponsor me to keep the updates and support going.</strong> PHP Monitor continues to be free and open-source, and this won’t change. If you are interested, you can <a href="https://nicoverbruggen.be/sponsor">sponsor me here</a>.</p>
]]>
        </summary>
        <category type="html">
            <![CDATA[]]>
        </category>
        <updated>Fri, 27 Jun 2025 12:26:45 +0200</updated>
    </entry>
    </feed>
