Arjen Wiersma

A blog on Emacs, self-hosting, programming and other nerdy things

The TLDR of this post is that I have release my application, Comicstash, as open source. I have been working on it for about a year now in various forms and have come to a point where the current version is something that is viable for the future.

A key concept has been “localfirst”. This means that there is no centralized server that maintains your data, but you, the user, are in control and responsible for your own data.

Comicstash Flutter

You can get development versions of the app from my forge: https://forge.arjenwiersma.nl/arjen/comicstash

Where it started

I started working on comicstash over a year ago. At first I worked on a Ruby on Rails web application. I modeled my database schema, and created several integrations with online resources such as Metron to pull in comic metadata. It looked great:

Comicstash Rails

The thing however was that I was not very happy with the language and the framework. I would rather use something else, so I started implementing a new version in Clojure, a language I love.

Going native

During the development time I became more interested in the idea of building something that is local first, meaning that it should not rely on centralized servers to handle data.

I would love for others to use my app, but I do not want them to have to deal with setting up a webserver and installing containers and having the hassle of maintaining that.

But with a move to the desktop I would lose the ability to use my app on my mobile device when on the go.

So, I started experimenting. I wrote a version in Qt using C++ and QML. This worked somewhat good, but the development proces was quite a hassle. Porting it to mobile from a proof of concept turned out to be quite an undertaking as well, so I left that idea.

Going through the motions of cross platform frameworks and toolchains I finally ended up with Flutter and Dart. Both I did not really know beforehand, but since I have started to quite appreciate both.

The final form

So, the app is now a Flutter/Dart application that builds from a single source (ideal for someone with little time to develop).

Comicstash Light

The application compiles to native code for the desktop and an app for Android.

The user data is kept on a single device, but you can sync it using (currently) WebDav. So if you have something such as Nextcloud you can connect it to the app and all the apps on all devices will remain in sync.

I am very happy with the syncing mechanism, it has stood the test of time (for now), going through several multi-device schema upgrades without issue.

Using AI

In this time it is important to tell you how I use AI. I am not against AI, but I am against AI Slop, where you just blindly generate code and just take it at face value.

I use AI by working out features and mechanisms, I let it write boilerplate code and it helps me with the UI. It is great at writing testcases for my code and generating infrastructures, such as the sandbox I use for local testing. The AI is never allowed to commit or autonomously do things, everything is tested and reviewed by me when it creates code.

As someone who works on this in my free time, those tedious tasks take away a lot of valuable time, now I spend that time on adding value to the app.

Go give it a try if collecting comics is your thing.

I have been thinking about the effect of AI on how I perceive activities that I previously found enjoyable. Specifically the effect on how I perceive coding.

I made the mistake (looking at it in hindsight) of allowing AI to take over my critical thinking when it comes to coding. Like many I found the wonder of AI in that it was able to very quickly write entire programs for me. The downside of this was that I felt no connection to the code whatsoever.

The lose of connection meant I actually did not really care about the quality of it and if I would actually use it lateron. This was a new discovery for me, I always pride myself in the code I write and have been able to explain design choices a long time after I wrote it. With AI I find that even after a few minutes you struggle to explain the structure that was created.

Structured frameworks like Ruby on Rails lend themself extremely well to the use of AI as it actually requires you to follow a predetermined path to build things. Writing in plain languages (from Java to C++ and back to Clojure), away from structure will get you into a world of hurt really quickly.

This is, of course, due to the fact that AI simply does not care. It does not care about maintainability, quality, change, and any other quality metric you can think of. It will just rewrite the entire thing when it is necessary.

I found this out when working on some code for a workshop I gave. I had claude code writing the entire thing, but when the credits ran out and I had a bug while facing a deadline I found a lot of duplication, hard wired dependencies and overly complex solutions to relatively simple problems.

AI is not an engineer. An engineer designs for change, cares about quality and takes care of the code base. AI does non of that. Granted you can write complete specifications and guidelines in the details of these constraints, just as the old-skool analist would do. But did you get into coding to become a business analist writing detailed specifications? I did not.

So, did I stop using AI? No. I use it just like I used to use stack overflow, to understand how to fix something that was not obvious from documentation. The documentation now is also produced by AI, it reads API documentation from a project and provides me with use-case specific documentation, which is great.

I am slowly getting back to enjoying coding again, by keeping AI on the sideline.

There are many people that really enjoy the speed of AI development, Omarchy is one of those projects. Kudos to them for enjoying that. For me the fun is in actually figuring things out and enjoying the building proces.

So, I went shopping at the IKEA and ran into some really neat home automation gadgets. The Grillplats plug especially interested me as I would like to track power consumption of devices, and I did not yet find a good solution for that. One impulse buy later I was home with my new Grillplats plug.

Of course I did a quick google search in the store to see if it was compatible with home assistant, and I found some articles explaining how to add it to your setup. So I was unworried when I started tinkering. That soon changed.

It turns out that one requires a Thread border router for these devices. Of course IKEA sells them, but would I rather no add another device to my network. Home Assistant it self has a connector that can be used, the connect zbt-2, which just plugs into the USB port of my Home Assistant Green.

I bought the ZBT-2 and connected it. Followed the instructions on screen and configured it for Threads. This is a well thought out integration.

And that is it, right? No. Here are the gotcha's that I ran into and how I fixed them.

IPv6

Matter / Threads uses IPv6 to do their communication. That means that your network has to support IPv6. On my Amplifi Alien router I enabled IPv6 under Internet and made sure the DHCP was working.

On my laptop I was able to confirm IPv6 and mDNS (the way the devices find eachother) was working by running avahi-browser -a, it shows all the mDNS requests on the network.

New installations of Home Assistant Green will not have the next issue, but mine is a bit older. I had to enable IPv6 for the docker instance on the Home Assistant by running ha docker options --enable-ipv6=true in a terminal. I installed a SSH app on the Home Assistant for this purpose.

Provisioning the device / WiFi

I learned that when you provision the device with your phone, it first uses the phone (Android) to do a lot of heavy lifting, including transferring credentials for the Thread network, it then switches over to the Thread border router to do the rest, while your phone connects to the device over WiFi.

These devices use 2.4GHz to connect on the WiFi spectrum. My phone is modern and uses the 5GHz band if it is available (higher bandwidth), so it went from “Discovering Device”, “Generating Matter Credentials”, “Confirming connection to Thread network X” to “Could not connect to device”. The last bit stumped me for a long while, but then I turned off 5GHz on my WiFi and tried again. It finally worked.

Conclusion

The plug is working now. The 8 euro buy turned into a 53 euro buy, but I am monitoring my energy usage on the plug now :D.

I wonder how the IKEA device does this, a non-technical person would not be able to resolve the issues I've had to get it going. Perhaps it builds its own WiFi network for the devices? If you know, do send me a message!

My last post was quite a while back. In all honesty, the current state of affairs in coding land had me somewhat burned out on building things, AI is taking the pleasure out of actually thinking.

I switched jobs in April, which also took a mental toll... so I decided to step away from my hobby projects and workshops to just focus on that.

Now, 3 months later, I decided to reset the way I work with my projects. I have started focusing on a single project, a desktop based comic book collection tracker. It is written using Qt and C++.

I don't use any AI, except for finding things in the documentation (replacing stack overflow and search engines I guess), and I am starting to enjoy the process again. Removing AI from my workflow was quite easy; simply use Emacs and stay away from more elaborate tooling.

In the project that I was running last year up to April we had to use Jetbrains IDEA, and it is loaded with things to make you stop thinking critically. I think that has been the biggest “oh man, is this what life has become?” driver. Luckily I am now able to leave that behind me. Obviously I could've just not used that tooling, but when everyone in a project uses it, it becomes a given really. In hindsight I now no longer remember all the things I did with it, while I can still remember that specific piece of code that I struggled with before the AI systems took over the thinking process.

Luckily Emacs does not force anything on you. The biggest helper I use in Emacs is lsp-mode.

Qt in Emacs

Qt comes with its own IDE, qt-creator, surprisingly mostly AI free. It has good completion for all the qml based syntax. Luckily lsp-emacs provides a good interface here as well. A combination of cmake and some elisp makes everything work together.

CMake

Qt applications build using cmake, and as a result you can make it generate all the necessary configuration you need.

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(QT_QML_GENERATE_QMLLS_INI ON)

The compiler commands will generate compiler_commands.json, which lsp will pick up and configure itself with.

Generating .qmlls.ini based on your cmake configuration will configure the QML Lanuage Server.

Emacs

The only thing that is left is to configure Emacs to support qmlls.

(eval-after-load 'lsp-mode
  (lsp-register-client
   (make-lsp-client :new-connection (lsp-stdio-connection
                                     (lambda ()
                                       (executable-find "qmlls")))
                    :activation-fn (lsp-activate-on "qml")
                    :priority -1
                    :server-id 'qml-ls)))

Thats it.

I have several Linux distributions running, on my laptop I use Omarchy and on my desktop I have NixOS. I love the hyprland configuration of omarchy and how it works beautifully straight out of the box.

One thing I use, however, is automatic theme switching. When there is plenty of light during the day I like to use a white theme. At night, I like to use a dark theme. Most Desktop Environments have some way to configure this switch to happen automatically. On Omarchy it turns out to be trivial to implement as well.

First, we need a package that triggers a change based on time and location. The most simple package is darkman, a simple script that will trigger a change based on where you are. You can either hardcode the location in a configuration or use geoclue for it. I chose to do some hardcoding in the config as the times that I am not in my timezone with my laptop are very limited.

The configuration lives in /home/user/.config/darkman/config.yaml:

lat: 52.3
lng: 4.8
usegeoclue: false
portal: false

It will then look at /home/user/.local/share/darkman and execute every script in that directory with a paramter of light or dark. A small bash script toggles between the two themes.

#!/usr/bin/env bash

case "$1" in
dark) omarchy-theme-set vantablack ;;
light) omarchy-theme-set white ;;
default) exit 1 ;;
esac

Do not forget to make the script executable.

To enable the service use systemd. I would enable it as --user, that way you separate out your services from the system services.

systemctl --user enable --now darkman.service

It is easy to check if everything works by calling status on the service.

~ ❯ systemctl status --user darkman
● darkman.service - Framework for dark-mode and light-mode transitions.
     Loaded: loaded (/usr/lib/systemd/user/darkman.service; enabled; preset: enabled)
     Active: active (running) since Fri 2026-04-24 12:32:14 CEST; 4s ago
 Invocation: 5a923b9f1e3341d987ac4c32a344be07
       Docs: man:darkman(1)
   Main PID: 44124 (darkman)
      Tasks: 16 (limit: 18743)
     Memory: 12.3M (peak: 69.2M)
        CPU: 1.165s
     CGroup: /user.slice/user-1000.slice/user@1000.service/background.slice/darkman.service
             └─44124 /usr/bin/darkman run

Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF Running without XDG portal
Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF Not using geoclue; using static location
Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF Sundown comes first; so it's day time
Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF Wanted mode is mode=light
Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF No transition necessary
Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF Next sunrise time=2026-04-25T06:22:38.364+02:00
Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF Next sundown time=2026-04-24T20:54:17.853+02:00
Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF Will set an alarm for sundown
Apr 24 12:32:14 mini darkman[44124]: 12:32:14 INF Setting timer seconds=30123 nanoseconds=202749121
Apr 24 12:32:15 mini darkman[44124]: 12:32:15 INF Script exited OK name=omarchy

Yesterday I spent the day in the north of The Netherlands, in Groningen. A little over 150km drive from my house, The occassion? I was invited to hold my Offensive AI workshop at BSides Groningen. The event had 230 visitors, of which 29 chose to follow my workshop. That is an amazing number, and the room was packed. Together they completed 124 challenges and had 197 conversations with the bots in order to find the hidden codes.

img

The time for the presentation was a bit short, 90 minutes. But we managed to fully go through Part 1 and I left them with Part 2 as homework. The presentation is available.

10 out of 10 for a conference, it was small but had a great atmosphere. The quality of the talks was extremely high and I ran into a lot of familiar faces.

img

My AI workshop is gaining traction! My next stop will be at BSides Grunn on April 17th.

Last year, I had a very successful session at JFall 2025, where the workshop was rated as the 5th highest talk at the conference. While the session was excellent, I felt it necessary to enhance my platform a bit further.

In order to host the workshop on attacking AI systems, it is important to provide a way for these challenges to be done in a controlled environment. Although there are some options for quickly coding online tools using AI, I believed it essential to create a dedicated platform that enables participants to navigate through custom challenges.

My first iteration was a Spring Boot (Java) tool designed to host the challenges. While it was an adequate setup, the interaction with AI and presentation of the challenges felt somewhat clunky. The next iteration is a significant step forward in terms of features and usability. It is written in Ruby on Rails, the language is not only straightforward to use, but the framework also allows me to focus on the aspects that truly matter. A key advantage behind the scenes is deployability; I can easily implement changes to my production platform. With Rails, it takes just a single command to push changes to production, just like in the 90s.

So, what did I add to my workshop?

  • UX: light/dark mode, depending on the system settings
  • UX: conversations with AI instead of one-off messages
  • AI: support multiple models in case on outages
  • AI: allow user to select a more difficult model to see how their solution works
  • Admin: a better dashboard to track progress

Next up is to create some really nice custom challenges for the fine folks at BSides!

I came across Boris Buliga posting about Vulpea v2. In it he describes how vulpea is now a separated package from org-roam, and his motivations for it. I have been using vulpea functions for a while with my org-mode files, so I was very interested. More interestingly I came across his packages vui.el and vino. With vui.el you can make interfaces in Emacs using the same pattern as React, and vino is a wine management app built on top of vulpea.

Then I thought; can I make a comic book tracker in Emacs? I love comics, I have quite a collection, especially in the Spawn francise…

img

Yes, that is the short answer. The code is extremely messy, I just wanted to get something working, but the gist of it is that you can create notes with meta-tags and link them together. Below is the raw content of my Spawn #1 issue.

- serie :: [[id:89adef5d-a8a5-41c2-915e-feb2ea5d2fae][Spawn]]
- number :: 1
- description :: While a strange assailant stalks the city, ripping out human hearts, another otherworldly being arrives. As his mind reels, our tortured hero remembers that he struck a deal with the devil in order to return to his beloved wife - five years after his death. The man who was Al Simmons is no more. He has become Spawn.
- cover :: ./comics/collection/data/99/3c95fb-f873-4fef-b3e5-9682162a227b/cover.jpg
- credits :: [[id:91e551e7-a1ee-4776-b326-70e8e586c643][Chris Ulm]] (Editor In Chief), [[id:063713be-97ca-4d0d-b6db-e0aacf6e5bff][Dan Danko]] (Production), [[id:a73ee9d7-a2ba-4a15-8426-b186f70cf9b3][Ken Steacy]] (Cover), [[id:d7ab28dd-8240-4582-b8f6-bf2cba7a0e86][Reuben Rude]] (Colorist), [[id:4c9cc732-eb5d-454d-b524-09ac6973451a][Steve Oliff]] (Colorist), [[id:5beb711e-4b7f-434c-91d0-a8bf1cabff92][Todd McFarlane]] (Story, Penciller, Inker, Cover), [[id:5546604d-03c0-4539-9541-63baeb2880b7][Tom Orzechowski]] (Letterer), [[id:b25b0fa3-22bd-45c3-aa7b-3e3aab1da4c9][Wanda Kolomyjec]] (Editor)
- characters :: [[id:d1a1e2f2-b492-4025-9a41-6d58e7e44f26][Brock Fennel]], [[id:c6bd48dc-c3b1-45d8-a140-fb3808fcc4ac][Jason Wynn]], [[id:98278c4d-004a-4559-860c-2e1ebe4924c7][Malebolgia]], [[id:b6eff357-b385-4245-bd72-844cf4e599ba][Sam Burke]], [[id:88f71833-84c8-41dd-bd29-2419a7bbc7af][Spawn (Simmons)]], [[id:0c32d357-ed71-4998-b3b4-830841f49d8a][Twitch Williams]], [[id:304d0b1a-4c7b-4c01-99a9-ae53a720a518][Wanda Fitzgerald]]
- price :: 1.95
- store-date :: 1992-05-10
- cover-date :: 1992-05-01
- metron-id :: 55499
- teams :: [[id:35343aab-75a9-4509-b992-538bebecf2aa][Hellspawn]]
- title :: Questions, Part 1

I used the Metron Cloud API to retrieve the metadata about the comic, then I create linked notes for the credits, characters and teams. It is now possible to see what characters show up in an issue, and in what other issues they are seen. For instance, these are the issues that Angela shows up in:

  [Spawn 9]
  [Spawn 26]
  [Spawn 28]
  [Spawn 30]
  [Spawn 31]
  [Spawn 40]
  [Spawn 43]
  [Spawn 44]
  [Spawn 45]
  [Spawn 46]
  [Spawn 47]
  [Spawn 48]
  [Spawn 49]
  [Spawn 50]
  [Spawn 51]
  [Spawn 55]
  [Spawn 57]
  [Spawn 62]
  [Spawn 63]
  [Spawn 65]
  [Spawn 75]
  [Spawn 89]
  [Spawn 96]
  [Spawn 97]
  [Spawn 98]
  [Spawn 99]
  [Spawn 100]
  [Spawn 101]

I love this setup, it gives me what I want in terms of the content and the accessibility through Emacs that is not a CSV. So I will build it out to a full app.

I like my modus themes for Emacs, and I thought it would be fun to bring them to my writefreely blog as well.

Below is the CSS you can drop in your Customize, it will automatically switch between light and dark variants.

/* =====================
   WriteFreely Modus Theme
   Auto light/dark via prefers-color-scheme
   ===================== */

/* Modus Operandi (Light) — default */
:root {
  --bg:          #ffffff;
  --bg-alt:      #f2f2f2;
  --surface:     #e8e8e8;
  --border:      #c6c6c6;
  --text:        #000000;
  --text-muted:  #595959;
  --text-faint:  #8a8a8a;
  --accent:      #8f0075;   /* Modus magenta */
  --accent-dim:  #c04fa8;
  --link:        #0031a9;   /* Modus blue */
  --link-hover:  #003497;
  --code-fg:     #005e00;   /* Modus green for code */
}

/* Modus Vivendi (Dark) — activated by OS dark mode */
@media (prefers-color-scheme: dark) {
  :root {
    --bg:          #000000;
    --bg-alt:      #1e1e1e;
    --surface:     #2a2a2a;
    --border:      #444444;
    --text:        #ffffff;
    --text-muted:  #ababab;
    --text-faint:  #6a6a6a;
    --accent:      #feacd0;   /* Modus Vivendi magenta */
    --accent-dim:  #a04080;
    --link:        #79a8ff;   /* Modus Vivendi blue */
    --link-hover:  #a8c7ff;
    --code-fg:     #44bc44;   /* Modus Vivendi green */
  }
}

/* Base */
body {
  background-color: var(--bg);
  color: var(--text);
  font-family: Georgia, serif;
  font-size: 18px;
}

/* Navigation / Header */
header, nav#blog-title {
  background-color: var(--bg);
  border-bottom: 1px solid var(--border);
}

header nav a,
nav#blog-title a {
  color: var(--text-muted);
  text-decoration: none;
  letter-spacing: 0.04em;
  font-size: 0.85rem;
  text-transform: uppercase;
}

header nav a:hover,
nav#blog-title a:hover {
  color: var(--accent);
}

/* Main content area */
section, #wrapper, #collection {
  max-width: 900px !important;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

/* Post / Article */
article, .post {
  background: transparent;
  max-width: 900px !important;
}

article {
  background: transparent;
  max-width: 900px !important;
}

/* Headings */
h1, h2, h3, h4, h5, h6 {
  color: var(--text) !important;
  font-family: Georgia, serif !important;
  font-weight: 700 !important;
  line-height: 1.3 !important;
  padding-top: 1em;
}

h1.post-title a,
h2.post-title a,
h1 a, h2 a, h3 a {
  color: var(--text) !important;
}

h1.post-title a:hover,
h2.post-title a:hover,
h1 a:hover, h2 a:hover, h3 a:hover {
  color: var(--accent) !important;
}

/* Links */
a {
  color: var(--link);
  text-decoration: underline;
  text-decoration-color: var(--accent-dim);
  text-underline-offset: 3px;
  transition: color 0.15s ease;
}

a:hover {
  color: var(--link-hover);
  text-decoration-color: var(--link-hover);
}

/* Paragraphs */
p {
  color: var(--text) !important;
  text-align: justify;
  text-justify: inter-word;
  hyphens: auto;
}

.description {
  color: var(--text) !important;
  text-align: center;
}

/* Blockquote */
blockquote {
  border-left: 3px solid var(--accent-dim);
  margin: 1.5em 0;
  padding: 0.5em 1.5em;
  color: var(--text-muted);
  font-style: italic;
  background: var(--bg-alt);
}

/* Code */
code {
  font-family: 'Courier New', monospace !important;
  font-size: 0.85em !important;
  background: var(--surface) !important;
  color: var(--code-fg) !important;
  border: 0px !important;
  padding: 0.15em 0.4em !important;
}

pre {
  background: var(--surface) !important;
  padding: 1.2em 1.5em !important;
  overflow-x: auto !important;
}

pre code {
  background: transparent !important;
  padding: 0 !important;
  font-size: 0.9em !important;
  color: var(--code-fg) !important;
}

.post pre, .post code,
article pre, article code,
#post pre, #post code {
  background: var(--surface) !important;
  color: var(--code-fg) !important;
}

/* Horizontal rule */
hr {
  border: none;
  border-top: 1px solid var(--border);
}

/* Post metadata / date */
.post-meta, time, .date {
  color: var(--text-faint);
  font-size: 0.82rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

/* Post list (index page) */
.post-listing, #collection .posts {
  list-style: none;
  padding: 0;
}

.post-listing li, .post-list-item {
  border-bottom: 1px solid var(--border);
  padding: 1.2em 0;
}

/* Footer */
footer {
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: 0.82rem;
  padding: 2rem 0;
  text-align: center;
}

footer a {
  color: var(--text-muted);
}

/* Read more link */
.read-more {
  color: var(--accent);
  font-size: 0.875rem;
  text-decoration: none;
  letter-spacing: 0.03em;
}

.read-more:hover {
  color: var(--link-hover);
}

/* Images */
img {
  max-width: 100%;
  height: auto;
  border-radius: 3px;
}

/* Selection */
::selection {
  background: var(--accent-dim);
  color: #ffffff;
}

/* Scrollbar (Webkit) */
::-webkit-scrollbar {
  width: 8px;
}
::-webkit-scrollbar-track {
  background: var(--bg);
}
::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--text-faint);
}

Finished in February

I finished reading/listening to the follow books:

  • Ewout: avonturen en inzichten van een eigenzinnige programmamaker
  • Het wordt ook steeds gekker. Stories by Lieke Hesters
  • De tindermoord: een swipe naar rechts wordt haar doodvonnis, het dramatische verhaal van Mieke Oort

Read this year

  • Je bent wat je doet by Ray Klaassens; a book about taking ownership in your life/work/everything
  • Nu of nooit by Dai Carter; a book on mental strength and tools how to stay the course in difficult times
  • Explosief; a short story by writing collective Crime Squad

Currently reading/listening

Currently reading The secret of secrets by Dan Brown.

Listening to Project Hail Mary by Andy Weir.