atomicules

Mostly walking the dogs

Notes On Building Hammerdb On Netbsd

Some initial rough notes on building HammerDB on NetBSD. It has quite a unique build system - I don’t know if it’ll be possible to make a Pkgsrc package of this. For now I just wanted to see if I could get it to build at all.

  1. See github changes
  2. Need to install 7-zip: sudo pkgin install 7-zip
  3. Need to install tcl 9 (just for the header file; Since HammerDB is actually building tcl this step feels superfluous, maybe the header is available locally when building and I just haven’t found it yet?): sudo pkgin install tcl90
  4. To build the pgtcl package need:

     export PG_CONFIG=/usr/pkg/bin
    
  5. I had to set this to get it to find my include files:

     export C_INCLUDE_PATH=/usr/pkg/include/python3.12/:/usr/pkg/tcl/9.0/include/:/usr/include/
    

    The last /usr/include/ isn’t actually required, but I chucked it on there for good measure

  6. And for the pty.h requirement somewhere along the way, we actually need util.h so I had to symlink:

     sudo ln -s /usr/include/util.h /usr/include/pty.h
    
  7. To actually build:

     cd /home/simon/Code/github/TPC-Council/HammerDB/Build/Bawt-2.1.0
     ./Build-Linux.sh x64 Setup/HammerDB-Linux.bawt update
    

    For now I’m just stealing/tweaking the Linux files instead of adding BSD ones.


At some point whilst trying to get things to build I did this:

    sudo ln -s /usr/pkg/bin/python3.12-config /usr/pkg/bin/python3-config

But on a recent rebuild to check through this post this step wasn’t required (I deleted the symlink before rebuilding) and I can’t recall why I did this in the first place. But leaving this as a note just in case


Running the builds

Basically you need to copy and extract the build somewhere:

cd ../BawtBuild/Linux/x64/Release/Distribution
cp HammerDB-5.0-Dev-Linux.tar.gz /to/somewhere/else
cd /to/somewhere/else
tar xvzf HammerDB-5.0-Dev-Linux.tar.gz
cd HammerDB-5.0-Dev-Linux
./hammerdb

I haven’t been able to get the prod one to work yet - for sure it’s got to be linked to “linking” and something has been stripped that shouldn’t have. Not too bothered about figuring this out yet because the Dev build does run!

HammerDB running on NetBSD

For the cli the (default) tcl one works, but the Python one (./hammerdbcli py) doesn’t - I’ve done some initial debugging (editing and putting print statements in; another benefit of the Dev build is you can just edit the “built”scripts) and it’s something to do with not being able to find tclpys even though it is there.


Before going down the building route I did have a brief play with the pre-built binaries and NetBSD’s Linux emulation, but:

/home/simo...-ubuntu-22  $ ./hammerdbcli
./hammerdbcli: /lib64/libc.so.6: version `GLIBC_2.33' not found (required by ./hammerdbcli)
./hammerdbcli: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./hammerdbcli)
./hammerdbcli: /lib64/libm.so.6: version `GLIBC_2.35' not found (required by ./hammerdbcli)

I.e. I didn’t have a modern enough glibc available, I only had 2.15 from the Suse Linux Compatibility package. I could, in theory, have tried tracking down a more modern one (because that’s how it works), but it felt easier to try to get the build working.

Exploring Syncing Notes With Fossil

For a long time I’ve toyed with the idea of replacing Simplenote with a Fossil repo of text files, more from an “owning your own data” point of view than anything else, but more “recently” since I stopped using Vim and switched to Helix. It’s probably not happening any time soon as Simplenote is still too convienent on mobile, but I thought I’d have a little play just because Fossil’s auto-sync is really neat.

  1. Created a repo on my (this) server, but a private one for now, not public
  2. Cloned that via ssh to my local machine: fossil clone ssh://me@linode/Code/fossil/repos/notes.fossil
  3. Then added a little “hook” to Helix via my mksh config like so:

     function hx {
       /usr/pkg/bin/hx $@
       if [[ $(pwd) == '/home/simon/Code/fossil/checkouts/notes' ]]
       then
         if [[ $(fossil extras) != "" ]]
         then
           fossil add $(fossil extras)
         fi
         fossil commit -m 'Auto-sync notes'
       fi
     }
    

    The idea being that if I am editing a note and then exit and save, Fossil will then automatically sync changes, including adding it if it’s a new note. The only downside to this is that if I suspend Helix (Ctrl+z) then that also triggers this and if I resume (fg) and then quit it won’t automatically sync.

  4. So I’ve also added a keybinding of fs to Helix to manually trigger a sync:

    [keys.normal.f]
    s = [":write", ':sh if [ "$(fossil extras)" != "" ]; then fossil add $(fossil extras); fi; fossil commit -m "Auto-sync notes"']
    

    (Slightly different syntax as it’s sh as opposed to mksh)

I can’t see me fully switching from Simplenote yet as I don’t have a good mobile solution (apart from using a sftp client on Android… hmmm… maybe), but it’s interesting to play about.

Patching the Synaptics driver on NetBSD to disable tap-to-click

This has bugged me since I got a ThinkPad X270, so much so that I actually found/made time for Frustration Driven Development, figured out a patch for the source code, built the kernel and tested it out. This has MASSIVELY IMPROVED my laptop experience. Honestly, not overstating that. Due to the Synaptics driver doing tap-to-click by default I would frequently make mis-clicks with my palm, etc when typing and would often mis-click and accidentally drag tabs, etc in Firefox. I was coming very close to disabling the trackpad altogether and using the pointer instead, but then “solved” half the problem by disabling mouse mode in Helix - although it was still annoying me elsewhere.

I patched it so tap to click is the default behaviour and I can just easily disable via sudo sysctl -w hw.synaptics.tap_to_click=0:

--- /usr/src/sys/dev/pckbport/synaptics.c.orig	2024-06-22 11:02:05.000000000 +0100
+++ /usr/src/sys/dev/pckbport/synaptics.c	2026-04-22 17:21:33.087414774 +0100
@@ -118,6 +118,7 @@
 static int synaptics_button_boundary = SYNAPTICS_EDGE_BOTTOM;
 static int synaptics_button2;
 static int synaptics_button3;
+static int synaptics_tap_to_click = 1;
 static int synaptics_two_fingers_emul = 0;
 static int synaptics_scale_x = 8;
 static int synaptics_scale_y = 8;
@@ -155,6 +156,7 @@
 static int synaptics_edge_motion_delta_nodenum;
 static int synaptics_finger_high_nodenum;
 static int synaptics_finger_low_nodenum;
+static int synaptics_tap_to_click_nodenum;
 static int synaptics_two_fingers_emul_nodenum;
 static int synaptics_scale_x_nodenum;
 static int synaptics_scale_y_nodenum;
@@ -865,6 +867,18 @@
 
 	if ((rc = sysctl_createv(clog, 0, NULL, &node,
 	    CTLFLAG_PERMANENT | CTLFLAG_READWRITE,
+	    CTLTYPE_INT, "tap_to_click",
+	    SYSCTL_DESCR("Enable tap to click"),
+	    pms_sysctl_synaptics_verify, 0,
+	    &synaptics_tap_to_click,
+	    0, CTL_HW, root_num, CTL_CREATE,
+	    CTL_EOL)) != 0)
+		goto err;
+
+	synaptics_tap_to_click_nodenum = node->sysctl_num;
+
+	if ((rc = sysctl_createv(clog, 0, NULL, &node,
+	    CTLFLAG_PERMANENT | CTLFLAG_READWRITE,
 	    CTLTYPE_INT, "two_fingers_emulation",
 	    SYSCTL_DESCR("Map two fingers to middle button"),
 	    pms_sysctl_synaptics_verify, 0,
@@ -1101,6 +1115,10 @@
 		if (t < 0 || t > 3)
 			return (EINVAL);
 	} else
+	if (node.sysctl_num == synaptics_tap_to_click_nodenum) {
+		if (t < 0 || t > 1)
+			return (EINVAL);
+	} else
 	if (node.sysctl_num == synaptics_two_fingers_emul_nodenum) {
 		if (t < 0 || t > 2)
 			return (EINVAL);
@@ -2293,7 +2311,7 @@
 	/*
 	 * Do gesture processing only if we didn't detect a palm.
 	 */
-	if (palm == 0)
+	if (palm == 0 && synaptics_tap_to_click)
 		synaptics_gesture_detect(sc, sp, fingers);
 	else
 		sc->gesture_type = sc->gesture_buttons = 0;

I haven’t noticed any downsides to this at all yet. I have physical buttons that I can use for clicking anyway PLUS it turns out if I press hard enough (I’d never done this because of tap-to-click) on the trackpad it actually physically clicks (and that still registers as a click with this patch).

I must be some kind of freak for hating tap to click (I hate it on macOS as well) since no one has bothered to change this over the years. Or even moaned that much about it.

I couldn’t recall trackpad usage on my previous NetBSD laptop being this bad, but then I looked up that hardware and realised it didn’t have a trackpad.


[EDIT: 2026-05-04] It got committed. Whoop!

Where am I running AI in Tmux?

Since using lots of Vim sessions in a terminal I had a brief dalliance with Nova (I did/do like it, but it’s not cross-platform) and then ended up with Helix. At work I tend to use Helix in the same way I was using Vim but instead of Terminal.app it’s Alacritty with one huuuuge Tmux session with multiple windows (“tabs”) per each Git repository I’m working on. I haven’t bothered figuring out renaming the tmux windows automatically and just rename them manually - it tends to be a very long running Tmux session that I also save and restore so renaming manually is a one-time thing. It is easy to find whichever repo I want (across 70+ windows) by using CTRL, b, w to bring up Tmux’s window search and then using / to search for the name I want.

Since starting to play about with AI clis, I’ll split the Tmux window so I have a pane for running the cli in and a pane for Helix. But I won’t be running an AI cli in all of these windows which has lead me to wonder, “How do I find where I’m running AI clis?”.

What I’ve done is add a simple wrapper to the AI cli command to set and unset Tmux window titles on launch and quit of the AI cli:

function ai {
	# Let me know where I'm running an AI cli session
	
	# Find tmux window this command was called from
	# This way can target window. 
	# And avoid racey style stuff where wrong "current window" gets renamed
	twin=$(tmux list-panes -a | grep "$TMUX_PANE " | cut -d : -f 2 | cut -d . -f 1)
	twse=$(tmux list-panes -a | grep "$TMUX_PANE " | cut -d : -f 1)
	# I don't think I'd ever use an asterisk as a window name, but this would fallover if I did
	twnm=$(tmux list-windows -a | grep "$twse:$twin:" | cut -d : -f 3 | cut -d ' ' -f 2 | cut -d '*' -f 1)
	tmux rename-window -t $twin "$twnm + ai"
	/path/to/ai/cli
	tmux rename-window -t $twin $twnm
}

And now I can easily search through my Tmux windows.

LINK: Using Gemini Cli For Software Development

Meant to link to this at the time, but I meant to write my own blog post (see yesterday) before that, but it’s taken me until now to do either; AI certainly isn’t freeing up any spare time in my life.

Playing About With Ai Clis On Netbsd

NetBSD themselves have a no AI policy (fair; at some point lawyers are going to make a mint), but I just wanted to have a play about and since I run NetBSD, perhaps for too long, I just assumed I was out of luck with most AI tooling (most things do just target macOS, Windows and Linux). But it turns out things are mostly ok thanks to all the CLI tooling being node/npm:

  • Gemini just works* (and requires nothing more than a Google account; Which I have again since switching to Android)
  • Codex throws an unsupported platform (I wonder if this could be worked around?)
  • Claude runs (but there is no free tier)
  • Qwen code works (requires a free account; Looks like a Gemini clone)

(Don’t use global installs)

I started with Gemini - I thought I’d jump in the deep end and ask it to do something useful and something fairly complex so I asked it to build me a Chrome Cast receiver in python - I’d love to be able to cast from my Pixel to my NetBSD laptop. It “worked” in that it built something that looked sensible and did run although didn’t actually work when it came to being a Chrome Cast receiver. So then I thought I’d step back a bit and ask it to migrate simplenote.py (long neglected by me) from Travis to Github Actions; Because this is the kind of thing I’ve been meaning to do for a few years and just haven’t found the time for; I.e. this seems like the kind of “chore” task we should be using AI for instead of all the terrible things we are using AI for.

Comparing Gemini with Qwen

For both I gave a prompt like this (apart from the branch name being different):

Create a branch of this repository called gemini-github-actions and migrate the Travis CI setup to Github Actions

And then also gave both permissions for whatever it asked for for the session.

Both gave me additional instructions. Gemini:

To verify the migration, you should push this branch and ensure the GitHub Actions runner can successfully execute the tests and build the package. If you want to proceed with a commit, please let me know.

Qwen:

Next steps:

  1. Add PyPI credentials to GitHub repository secrets (PYPI_USERNAME and PYPI_PASSWORD)
  2. The deploy job triggers on release publication (modern approach vs. Travis’s tag-based deployment)

Personally I find it dumb that neither of them committed automatically (why would I be telling you to create a branch?) so I told both:

Please commit your changes including the summary of the changes you’ve just provided in the commit message.

Qwen had just created the .github/workflows/ci.yml file whereas Gemini also removed the .travis.yml and updated the README badges (nice); However, I LOVE that Qwen co-authored the commit by default.

I told Qwen:

Please also delete the .travis.yml and update the README badges to suit Github Actions ​

to get it in the same state.

(Interesting aside… I had to update my PAT to allow creation and updates of workflows before I could push)

Here’s the final results (final at the time of publishing is one commit on the gemini branch and two on the qwen one):

  1. Gemini
  2. Qwen

There are some subtle differences. Gemini includes many more python versions, Qwen just two, matching exactly what I had for Travis.

Of these two, only the Gemini branch automatically triggered a Github Actions run because it sensibly included its own branch - it failed though on every single job. Don’t actually know why. Github just tells me all the jobs exceeded the maximum execution times whilst waiting for runners.

In an effort to actually publish this post this year I am going to leave the AI efforts there for now - I might continue with both branches and get Gemini and Qwen to make further tweaks or I might continue by hand, but that could take me days, or weeks. Or months. So this will do for now. There’s no conclusions to this post, just observations - literally just me documenting my playing about.

This post has taken me months to write. I’m just too busy. I’ve been wanting to play with AI CLI tooling because up until just the last day or so I’ve not been allowed to use any such tooling at work (believe it or not). At least my tinkering on NetBSD did give me a little bit of a headstart and familiarisation.


* - Worked until this change since that it now gives env: unknown option -- S on NetBSD. Reverting that makes it work again. For me that was these files:

  • /home/me/node_modules/.bin/gemini
  • /home/me/node_modules/@google/gemini-cli/dist/index.d.ts

Making Use Of Having Netbsd Hardware Again

It took me a bit to get up and running after getting the laptop, but did want to point out that I am actually making use of it now:

  • I make use of the excellent pdh on my NetBSD server so it means I can somewhat have an unofficial PagerDuty client on my phone (for “reasons” I can’t use the official one). But it bugged me that plain output was broken and that I couldn’t snooze alerts. So I fixed them both; Technically I didn’t need local NetBSD hardware for this since I only run pdh on my server, but just having a portable computer which is mine doesn’t half make it easier to do this.
    • I spent quite a bit of time looking at pre-commit and ruff-pre-commit as part of this. In the end I’ve not pushed anything, but I patched pre-commit locally so it works after I install pysqlite3, but then still ran into issues building ruff-pre-commit on NetBSD and I couldn’t find a work-around even though I could install ruff itself fine.
      diff --git a/pre_commit/store.py b/pre_commit/store.py
      index 1235942..ba2469a 100644
      --- a/pre_commit/store.py
      +++ b/pre_commit/store.py
      @@ -3,7 +3,11 @@ from __future__ import annotations
       import contextlib
       import logging
       import os.path
      -import sqlite3
      +try:
      +    # Primarily for BSDs that don't have built-in
      +    import pysqlite3
      +except ImportError:
      +    import sqlite3
       import tempfile
       from collections.abc import Generator
       from collections.abc import Sequence
    
  • One of the first things I did on this machine was build helix. It built fine outside of Pkgsrc, but I wanted to update the Pkgsrc package and having local NetBSD hardware meant I could finally do it. Now I just need someone to merge it.
  • Similarly with spotifyd, especially since I’m the supposed maintainer of the Pkgsrc package. That really can only be tested on local hardware.
  • And libvips too.
  • I had some thoughts about trying to setup my own taskwarrior sync server and put together a Pkgsrc WIP package for taskchampion-sync-server, but I don’t think I’m going to pursue this as using the mobile app requires running CCsync as well which just seems silly. Maybe I’ll re-visit this at some point. Maybe I’ll go back to TeuxDeux.

Not Computerless

Not Computerless

Finally! Took me just over three years to save up for a old Thinkpad X270 (~£85). It seems to be in excellent condition and interestingly, despite being a bit more chunky than my work’s Macbook Pro M1 14”, it feels a lot lighter. And arguably has a better keyboard. Also, it might be a seven year old machine, but for a progression in NetBSD hardware for me that has gone from Pentium III, to Pentium D this thing feels like the future!

My plan was that I’d get a X270 or T470 (but not anything newer) as that way I could put the SSD I’d pulled from my old Optiplex into it and immediately have a working NetBSD machine that I could then upgrade. But after three years that SSD was unlikely to work and things had moved on a fair bit. So I opted for a fresh NetBSD 10.1 install (after a final boot into the Windows OS it came with to update the BIOS). I mostly followed this approach (encrypted swap comes free with NetBSD 10), but with these changes (these notes are more for me than anything):

gpt add -l efi -t efi -s 128m ld0
gpt add -l netbsd -t ffs -s 20g ld0
gpt add -l swap -t swap -s 8g ld0
gpt add -l netcgd -t cgd ld0

On my NetBSD server I have 5Gi /root and 10Gi /usr. Here I want /usr unencrypted (because I just do), so I’ve opted for 20Gi so I have a bit of breathing room. For cgd I have:

/dev/cgd0b  /tmp  mfs     rw,-s=132m    0 0
/dev/cgd0e  /var  ffs     rw            1 2
/dev/cgd0f  /home ffs     rw            1 2

(I set a to unused).

I have 2Gi for /tmp and 10Gi for /var. I kind of wish I’d done a bigger /tmp directory as I’ve already maxed it out with some cargo builds (but hopefully only because it didn’t clean up some previous attempts); Have to be honest that I don’t fully understand the /tmp changes with NetBSD 10. I seem to have both a /tmp and a /var/shm tmpfs - I don’t know if that’s intentional or I’ve just done something wrong. Maybe some day I could change this, but probably not any time soon.

I used to use dwm and st, but since I was setting up from scratch I decided to give leftwm a whirl to go with alacritty which I’ve been using on macOS for the past few years. I do like that dwm is “self-contained”, whereas with leftm you have to worry about status bars, etc. I’ve opted for lemonbar after making a small sed change (sed -rg '/^\s*$/d') in the script for NetBSD. I’m showing battery usage based on this approach (I have two batteries though!) and built this little status script:

#!/bin/sh
# file:~/bin/leftwmstatus
while true; do
        load=$(cat /proc/loadavg | awk '{print $1, $2, $3}')
        # Memory Free / Swap Free
        mem=$(cat /proc/meminfo | awk 'FNR==5{printf ("%.1f/", $2/1024/1024)};FNR==10{printf ("%.1f GB", $2/1024/1024)}')
        cal=$(date +"%a %b %d %Y %H:%M")
        batt1=$(envstat -s acpibat0:charge | grep charge | sed 's/.*(\(.*\)%)/\1/')
        batt2=$(envstat -s acpibat1:charge | grep charge | sed 's/.*(\(.*\)%)/\1/')
        charge=$(if envstat | grep -Eq 'connected.*TRUE'; then echo ^; fi)

        printf "S$batt1%%+$batt2%% $charge | $load | $mem | $cal\n"
        sleep 2
done

A few random “issues” I’ve had:

  • Needing to set XDG_RUNTIME_DIR for leftwm
  • I’ve not been able to enable XDM yet because I hit keyboard input conflict issues.
  • An occasional fsck issue on boot I have no clue about because it’s moaning about fsck_home being missing (which doesn’t seem to be a thing).
  • I had TLS/SSL errors doing the initial install with pkg_add. I can’t actually remember how I resolved this. I think I first of all had to set pkg_path to use http to get the Mozilla root certs and then I could switch it to https.
  • Suspend doesn’t work. At home this isn’t a huge problem as I just leave it turned on.
  • I have somewhat got the /etc/powerd/scripts/acadapter script working to lower the frequency when on battery (sometimes it doesn’t trigger, I don’t know why; Not a massive deal).

At the time I set this up the Pkgsrc version of Helix was set as broken, but building it outside of Pkgsrc was easy; I am going to see if I can get Pkgsrc Helix updated now I actually have NetBSD hardware to do that on.

I’ve got it setup enough that I can use it, but still need to tweak a few things:

  1. Finding a good way to type £ (it’s an American keyboard).
  2. Disabling taps on the track pad because I keep clicking accidentally.

I’m very happy to have NetBSD hardware again!


[EDIT: 2026-04-03] I probably should have used a bigger partition for root (and thus /usr). I kind of thought “I get away with 10GB on my server so 20GB will be fine”, but without realising I’d want to install a lot more stuff on a laptop. I wonder if I could move this to the encrypted partition for more space? Hmm…

LINK: And Repeat

I am making a point of taking my whole 1.5% allowance this year, but my months go something like this:

  1. January: Save money for…
  2. February: Mum’s birthday, Dad’s birthday and Valentine’s day.
  3. March: Mother’s day and paying some of February off
  4. April: Catching a break (but typically some cheap/second hand running shoes at this point) and also probably paying for some of February.
  5. May: A’s birthday
  6. June: Father’s day
  7. July: ?
  8. August: ?
  9. September: ? (Maybe saving money for a Christmas present for A).
  10. October: Probably another pair of running shoes
  11. November: Saving money for…
  12. December: Christmas presents for Mum and Dad.

So I’ve two to three months where I could save. I’m trying to be more aware (writing this out has helped me) and (even more) realistic this year: Stuff for A shouldn’t be included on this budget, but seemingly is; Technically one pair of running shoes would do for the total distance I run, but they also get used for dog walking; Second hand running shoes from Vinted help save cash; April and October give me six months per pair of shoes and also kind of works seasonally.

If nothing else crops up in July and August then I shouldn’t have to repeat this post again next year.

Taskwarrior 3 on NetBSD Notes

If I have time (“ha ha ha ha”; I’m also doing this all via ssh from my phone) I’d like to see about making a Pkgsrc WIP package for this (since it’s quite a change from v2 it makes more sense to stick it in WIP rather than just upgrade the existing Pkgsrc package). On that note, I wonder if it’ll have to be split out into taskchampion and taskwarrior for Pkgsrc? I.e. so the new Rust taskchampion bit is built first and then the C++ taskwarrior bit includes that?

In the meantime I just built outside of Pkgsrc:

  1. Needed a small patch to build:

     diff --git a/CMakeLists.txt b/CMakeLists.txt
     index 25ed4d9eb..a644268ac 100644
     --- a/CMakeLists.txt
     +++ b/CMakeLists.txt
     @@ -65,7 +65,11 @@ SET (TASK_RCDIR "${TASK_DOCDIR}/rc" CACHE STRING "Installation directory for con
      SET (TASK_BINDIR  bin            CACHE STRING "Installation directory for the binary")
         
      # rust libs require these
     -set (TASK_LIBRARIES dl pthread)
     +if (NETBSD)
     +  set (TASK_LIBRARIES pthread)
     +else ()
     +  set (TASK_LIBRARIES dl pthread)
     +endif ()
         
      check_function_exists (timegm  HAVE_TIMEGM)
      check_function_exists (get_current_dir_name HAVE_GET_CURRENT_DIR_NAME)
    
  2. Needed this exporting: CARGO_HTTP_CAINFO=/usr/pkg/share/mozilla-rootcerts/cacert.pem
  3. And then a cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_RPATH_USE_LINK_PATH:BOOL=ON . to keep libuuid linked for the install; I am wondering if libuuid is even needed on NetBSD, probably not, but that can be an exercise for another day.
  4. The rest of it is per the INSTALL instructions.

I also updated my patch from ten years ago to fix recurring tasks for DST for v3:

diff --git a/src/recur.cpp b/src/recur.cpp
index d6bb47454..b11480245 100644
--- a/src/recur.cpp
+++ b/src/recur.cpp
@@ -53,6 +53,8 @@
 
 // Add a `time_t` delta to a Datetime, checking for and returning nullopt on integer overflow.
 std::optional<Datetime> checked_add_datetime(Datetime& base, time_t delta) {
+  Datetime recurrence_date;
+
   // Datetime::operator+ takes an integer delta, so check that range
   if (static_cast<time_t>(std::numeric_limits<int>::max()) < delta) {
     return std::nullopt;
@@ -62,6 +64,14 @@ std::optional<Datetime> checked_add_datetime(Datetime& base, time_t delta) {
   if (std::numeric_limits<time_t>::max() - base.toEpoch() < delta) {
     return std::nullopt;
   }
+  // Shift current to midday, to be safe of any DST changes, before calculating future date
+  // Then set back to correct hour
+  // Only do this if delta is a whole number of days
+  if (delta % 86400 == 0) {
+    recurrence_date = (Datetime (base.year(), base.month(), base.day()) + 43200) + delta;
+    return Datetime (recurrence_date.year(), recurrence_date.month(), recurrence_date.day()) + 3600 * base.hour() + 60 * base.minute() + base.second();
+  }
   return base + delta;
 }

It seems there maybe future plans to use timezones in tasks which would finally solve this properly so I guess since my approach is still a little hacky it’s not worth PR’ing (the idea didn’t get any traction in the last ten years anyway).

These are the ten most recent posts, for older posts see the Archive.