Sander van Dragt's Notes

Sander @ 22 hours ago

Lamb logo experiment: using a cursor as the L

Sander @ Wednesday at 4:11 pm

Trying out a new 2024 theme I've started working on. It's not super clear yet where a new post begins, and it's a bit bland. #lamb #projects

Sander @ Friday at 8:39 am

Bandcamp celebrated their 40th birthday last month by removing its most useful feature, it's no longer a BandCamp Friday every month!

Sander @ Friday at 8:29 am

Most mobile “games” are like being a Sponsor on the Hunger Games: spend money dropping in some booster food for the contestant, who you have no control over. Is it time to reclassify these premium currency manager apps under the Finance category they belong to? Using their family's credit card, it's training multiple generation of kids to become hedge fund managers gambling their own future. My review of NASCAR Manager: 0% interest gained #games #iap

Sander @ April 19 at 10:02 am

Schrödinger's login: if I am able to comment as a member, but see a login form when I click on my account, was I logged in or not?

Sander @ March 13 at 12:37 pm

Tried 1Password for SSH keys and Git management (keys in 1Password, authentication via agent + desktop prompt before access, and 1Password must be running) and not a fan of more red tape around the SSH keys. Non-interactive processes just fail to use the key first-time round, so I ended up having to commonly do operations twice. Easy restore from a BackInTime backup. #security #linux

Sander @ February 21 at 11:07 am

Navigate Xbox Series console using LG remote

To navigate an XBOX series games console using an LG Magic Remote, first disable the Automatic Configuration option in the Connection settings under HDMI Configuration in the TV's settings. Reconnect the XBOX's HDMI cable going into the TV. Then on the input switcher, edit the Xbox setting, mark it as a Blu-ray player manufactured by Microsoft, and follow the wizard. #technology

Sander @ February 12 at 10:38 am

Looking at my browser tabs, I can't believe that we collectively have still not standardized on page title vs site title formats in 2024. I've seen SITE - PAGE, PAGE / SITE, PAGE | SITE, SITE: PAGE, PAGE etc. I'd prefer it if we had a sitetitle tag in head and let the browser figure it out. #technology #html

Sander @ January 31 at 2:30 pm

Content vs crap. A single sentence marked with a red box, versus the rest of the site wasting screen space #technology

Sander @ January 31 at 11:57 am

Use an expression to select an array value in PHP. It's handy for quick output logic. Update: it's a bit too clever, perhaps, versus using the tenary operator.

$classes .= [ ' sort-default', ' sort-custom' ][ has_custom_sort( $post ) ];

#php #technology #tips

Sander @ January 23 at 11:21 am

Why do web-browsers not have a dedicated "Back to top" button? #technology

Sander @ January 23 at 10:11 am

I replaced my cold half finished coffee with a hot half finished coffee.... because the machine ran out of water 😂

Sander @ January 16 at 4:33 pm

Finally updated my #WordPress Blocks In Plain English page with the WordPress 6.3 change in naming from Reusable Blocks to Synced Patterns. #technology

Sander @ January 16 at 10:16 am

Created a HTMX PHP sandbox to get started really easily. Feedback welcome. Might add some convenience functions #projects

Sander @ January 10 at 9:07 am

I've updated my one stop system update script to remove unused Flatpaks. #linux #projects

Sander @ January 3 at 2:07 pm

What would it take to make writing vanilla JavaScript more pleasant? After reviewing the code for my blogging engine Lamb I concluded:

Most of the code adding interactivity to personal websites comes down to running code after the page has loaded; niceties to query the DOM and hook into events. There's probably more but this is a start. The result is shorthand.js. Hint: it's not dissimilar to jQuery, but you can fully learn it in 5 minutes.

Which simplifies code to:

onLoaded(() => {
    const forms = $$('form.form-delete')
    forms?.forEach($form => $form.on('submit', ev => {
        let confirmed = confirm(`Really delete status ${}?`)
        if (!confirmed) return

This is just a prototype and will evolve.

#technology #projects #lamb

Sander @ January 2 at 4:37 pm

If you're using the Anytype (current version at the time of writing: 0.37.3) AppImage and made a .desktop file to launch it, here are a few tweaks that integrate it into your system better.

You may want to use a program like Desktopius to edit the .desktop file.

The Anytype icon and part of the main window

Prevent two dock icons

If you see one item for the AppImage and one for the application window in the Dock, add StartupWMClass=anytype and these will be combined. This value is the result of running xprop WM_CLASS and clicking on the Anytype main window.

Fix the icon

If you see the generic icon, extract the icon to a path and update Icon=/path/to/anytype.png (replacing the path to the directory where you will extract the icon).

To extract the icon from the AppImage do the following:

$ cd /tmp
$ mkdir anytype && cd anytype
$ /path/to/Anytype.AppImage --appimage-extract
$ cp squashfs-root/usr/share/icons/hicolor/1024x1024/apps/anytype.png /path/to/anytype.png

#technology #anytype

Sander @ January 2 at 9:59 am

PHP-Activate 0.3.1

I've released php-activate 0.3.1, a PHP project version manager for Linux systems using native PHP packages.

Made for those working with multiple projects using a variety of PHP versions, this script will switch to the correct PHP version for the local shell session. It does not require sudo, so even works in IDEs.

The latest version contains a few small changes:

  1. Eliminate the activate_php:export:20: invalid option(s) output. Exporting the php function is not needed as we source the script into the current shell session.
  2. Harden the script by implementing shellscript linting recommendations.
  3. Readme update.

#projects #phpactivate

Sander @ December 15 at 9:47 am

PHP-Activate 0.3.0

I've released php-activate 0.3.0, a PHP project version manager for Linux systems using native PHP packages.

Made for those working with multiple projects using a variety of PHP versions, this script will switch to the correct PHP version for the local shell session. It does not require sudo, so even works in IDEs.

The latest version drops support for the Fish shell again, and fixes compatibility with direnv. The correct PHP version is activated when changing into a project directory that is setup according to the README. #projects #phpactivate

Sander @ December 11 at 8:51 am

I experimented with _hyperscript to replace the little JS enhancements on the Lamb microblog over the weekend. After a day I had all the scripts replaced with clearer code in about half the lines of the previous JavaScript. It sounds like a success, however I rolled it all back. This is because _hyperscript requires a 100KB library, whereas the 3KB JavaScript it replaces does not. As Lamb is a minimal blogging system, that's the vast majority of the pagesize. #technology

Sander @ December 6 at 2:28 pm

Lamb image test

Image upload support has landed in the Lamb repo:

Drag images into the composer textarea and they will be automatically uploaded behind the scenes and inserted into the post as markdown, similar to how GitHub works:

Peek 2023-12-06 14-36.gif

This has been the main missing piece for me, so I'm very pleased. #lamb #projects

Sander @ November 14 at 10:08 am

Ashley Belanger writes for Ars Technica Apple gets 36% cut of Safari deal with Google as default search:

Statista reported that Google's advertising revenue was $224 billion in 2022, and based on that, Engadget estimated that Apple likely gets paid in the tens of billions of dollars for Google's default Safari placements.

Meanwhile Slack, Discord and others continue not to benefit from an Electron commit that wasn't properly reviewed setting Search with Google as the default #technology #privacy

Sander @ October 27 at 1:59 pm

There is no queue, I play one song: Spotify adds the next song in the playlist in the queue. There is one item in the queue, I add another song to the queue: Spotify replaces the queue with the song.

Sander @ October 17 at 10:45 am

I like to have pinned browser tabs on any window (mail, calendar etc) that do not disappear. These two together help with that:

Sander @ October 5 at 8:59 am

PHP-Activate 0.2.0

I've released php-activate 0.2.0, a PHP project version manager for Linux systems using native PHP packages.

Made for those working with multiple projects using a variety of PHP versions, this script will switch to the correct PHP version for the local shell session. It does not require sudo, so even works in IDEs.

The latest version adds support for the Fish shell. #projects #phpactivate

Sander @ September 26 at 10:24 am

When you want to create an exception to a add_header directive in nginx that is declared in an include, just declare it before it, as successive declarations of the same header are ignored. #til

Sander @ September 22 at 9:12 am

On personal goals

Today, I've realised my overarching goal as a software engineer: create digital experiences that help improve mental health for the people using them.

Unwittingly I myself have trended towards community software where capitalism, media manipulation and corporate influence is minimised (open-source, privacy first, self-hosting), and writing software with these values in mind.

In my experience, capitalism and corporate software distort the incentives of software for the financial benefit of it's backers. Media manipulation uses information overload and content agendas to influence the news cycle and change the common values we live by. This ultimately is more important to them than the wellbeing of the person experiencing the artifact, but not to me.

How have I been unwittingly trying to create positive user experiences:

#technology, #projects, #society

Sander @ September 19 at 1:52 pm

Made a Linux shell script called BreakAware that calculates the time since your last break as I couldn't find an alternative to Pandan (macOs).

BreakAware is a lightweight and user-friendly Fish shell script designed to help you maintain a healthy work-life balance. It tracks the time since your last break, whether it's due to PC sleep, hibernation, logout, or session locking. BreakAware empowers you to be more aware of how you spend your time and encourages you to take regular breaks, enhancing your productivity and well-being.

#projects #breakaware

Sander @ September 9 at 12:56 pm

If you want an example of why we need a viable open source phone ecosystem, try and share some assorted music files from your non-Apple pc to another person’s iPhone. #technology

Sander @ September 7 at 9:50 am

Updated my Notetaker tool rundown, with a new current pick.

Sander @ August 25 at 11:49 am

RAM Usage Nerdery

I've been wondering how much RAM I used on my 32GB Ubuntu derivative workstation, mainly used for web development using docker containers and light vm experimentation. Yes when I spotcheck with htop I have enough RAM free, but do I actually need 32GB, when I'm in the market for a new device or an upgrade?

It turns out, yes I do ideally.

The setup

On startup I've been running this script to save the memory stats into a csv file:

#!/usr/bin/env bash
while true
  do free  | grep Mem | tr --squeeze-repeats ' ' ',' | tee --append ~/memory.csv
  sleep 10

Today (after 15 days) I pulled the stats into a database and browsed it:

sqlite-utils insert /tmp/mem.db memory-stats memory.csv --csv
datasette /tmp/mem.db

Sorting by the available column shows that the lowest value I get is 11.9GB available to be freed.

The available column is the sum of the Free column plus the portions of the Buffers and Cache columns (or the Buff/cache column) that can be relinquished immediately. The Available column is an estimate, not an exact figure.

I think it's a reasonable measure. This system does not have zram memory compression enabled, that would help probably.

It has 4GB swap, I did not measure it's usage.

Hopefully this review helps for people who want to measure their own usage.


Sander @ August 16 at 10:10 am

#TIL you can search GitHub wikis by using the search bar and then filtering by wiki. That's almost as nice as keeping your notes in a Lamb instance ;-)

Sander @ August 16 at 7:13 am

We really now need a norobots.txt / terms of use for a persons identity to centrally state how a persons information can be not used by big tech. Something linked to indieauth perhaps?

Sander @ August 3 at 9:03 am

Anchor bugs in Blink engine browsers

I've been looking into an issue for work where Google Chrome loads a URL ending in an anchor and scrolls to the incorrect position on the page. Imagine linking to the third heading on a page and Chrome scrolling to a indeterminable position above that. Firefox is not affected.

After ruling out all JavaScript and the particular HTML source code, it turns out that disabling the html { scroll-behavior: smooth; } CSS style fixes the issue. My theory so I suspect there is a race condition between determining the length of a page and position of the element while scrolling? This results in bugreports such as "Anchorlink going to the wrong place"

Regular behaviour is also partially broken

What's more interesting is that blink based browsers are unable to scroll to the anchor even after the page is already loaded!


  1. load This page has an id attribute test set on the header but is otherwise plain text. Any other page will also work though.
  2. The page loads and the scrollposition is correctly set at the test header.
  3. Scroll up a paragraph or more than a screen, and refresh the page. Notice the scrollposition isn't set.
  4. Optionally refresh many times, or press the Enter key in the address bar to attempt to reload the page. However the scroll-position is now permanently broken until you navigate away from the page.

This results in bug-reports such as "It only goes to a place when you first enter the URL to the browser, it doesn’t go to the same place when you re-fresh the page". Together this erodes the trust in the application and honestly it's hard to believe this has not been more widely reported.

There's probably an elaborate JavaScript solution but in my opinion a browser's bugs should be fixed by the browser, because in the future when the browser fixes itself a workaround might result in incorrect behaviour. #technology

Sander @ July 28 at 11:43 am

Lamb 0.3.0

Introducing Lamb 0.3.0 - Literally Another Micro Blog. This release brings new features, improvements, and bug fixes, making blogging even easier. The update includes Docker support, an optional config.ini with support for menu items to customize your installation, improved documentation, and more.

Lamb offers a simple, self-hosted single-author blog with a Twitter-like interface, friction-free Markdown entry, discoverable Atom feed, hashtags support, and a 404 fallback URL feature.

Download the latest release from GitHub and provide your valuable feedback. #lamb #projects

Sander @ July 16 at 6:41 pm

Here’s my July picks for my first Sixty Minutes Slice playlist. Hope you like the music!

. #playlist

Sander @ July 6 at 2:03 pm

The fixed line fiber broadband market is currently dysfunctional with too many players struggling to compete and see a return on investment. It is time for the UK industry to collaborate in a smarter way to provide all users with a better service," he said.

UK’s bigger than London.


Sander @ June 17, 2023 at 7:33 pm

Fafi 0.2.3

Fafi is a bookmark indexing and search tool.

Install the fafi command line app easily using pipx install fafi that's all that's needed, and then feed it a single URL, a text file with one URL per line, a bookmarks.html containing links (any browser's export) or --firefox and it will detect the Firefox profile and extract its bookmarks and index them.

Then search all your bookmark contents using full-text search and ranking!

New in 0.2.3

The release notes for version 0.2.3 of Fafi (Favorites Finder) include the following changes and updates:

View on GitHub

#projects #fafi

Sander @ June 16, 2023 at 11:23 am

Workaround for when poetry cannot find pyenv managed python

I use pyenv to manage python versions for my projects, and use poetry to manage project dependencies. Poetry was unable to find the latest python version, and insisted on creating virtual envs with system python.

Setting poetry config virtualenvs.prefer-active-python true did not work for me, and neither did poetry env use whatever-version:

$ poetry env use 3.11
Could not find the python executable python3.11

However pyenv can list the the full path to the currently active python, which can be used as a parameter to poetry, so the following worked:

$ poetry env use $(pyenv which python)
$ poetry install; poetry run python --version

Update: double check that pyenv is configuring the shell correctly -- $HOME/.pyenv/shims should be in your PATH variable, and your shell runtime configuration file (such as .bashrc / .zshrc) should contain eval "$(pyenv init -)"

Hope this helps!

#poetry #python #pyenv #ubuntu #elementaryos

Sander @ June 6, 2023 at 3:11 pm

In Sublime Text you can hold shift and select another tab and it will split the view! wow.

Sander @ April 12, 2023 at 10:39 am

If you also find the Leave button in Slack Huddles moves about a lot at the end of meetings, you can click on the headphone toggle in the bottom of the left sidebar to leave a huddle. (or try your luck at russian roulette and press the shortcut which either starts, joins, leaves or ends a huddle)

Sander @ April 11, 2023 at 3:08 pm

Turns out yaml_parse() in PHP automatically parses a front matter block out of a markdown based post.

Sander @ April 10, 2023 at 11:04 am

Entry titles aren't optional in Atom feeds, how to micro blogs deal with that? Update: it seems by making the title an empty element.

Sander @ April 10, 2023 at 10:41 am

The AI evolution seems scary until you realise most of the applications are summarised as a wrapper around the OpenAI API that does all the work, with the context provided by a prompt prefix. So with half a day's study most programmers can participate. #technology

Sander @ April 6, 2023 at 10:43 am

Ubuntu's minimal installation appears to be the standard installation plus the removal of some packages! That's unexpected as I was trying to save installation time. #ubuntu #technology

Sander @ April 4, 2023 at 1:55 pm

Map Eject to Multitasking View

So I use ElementaryOS and in version 7 you are no longer able to map just the Eject key. I like to map this to the Multitasking View (something like the macOS Expose).

The solution is to use the DConf Editor application and search for show-desktop. ElementaryOS repurposes the show desktop to the multitasking view. Simply set the value to ['Eject']

Alternatively you can set the left-super key (windows/command) to the Multitasking View. This seems better supported.

#elementaryos #linux

Sander @ March 24, 2023 at 9:46 am

Lamb 0.2

I've released Lamb 0.2, my micro blogging app that's powering this site.

What's new?

#lamb #projects

More info and download link

Sander @ March 24, 2023 at 7:24 am

Anyone, regardless of coding experience can now create scripts and simple WordPress plugins:

WordPress developers who want to share their AI-assisted creations with the community have also started submitting them to

We can imagine the headlines in six months when another popular site or plug-in is hacked, when both the author and the generator and the webmaster don’t understand the code, and can’t review it for security weaknesses, The current tools are proof of concept creators, but please be careful not to assume production ready code.

If you want a code review done hit up an engineer, like myself #technology #ai #wordpress

Sander @ March 21, 2023 at 2:28 pm

I've released php-activate 0.1.2, a PHP project version manager for Linux systems using native PHP packages.

Made for those working with multiple projects using a variety of PHP versions, this script will automatically switch to the correct PHP version after cding into the project folder. It does not require sudo, so even works in IDEs.

The latest version contains only documentation changes, but I've been using this successfully for a few months, so wanted to share this more widely. #projects #phpactivate

Sander @ March 21, 2023 at 12:41 pm

Basic routing using REQUEST_URI

So for nginx it is not straightforward to setup PHP-FPM so that PATH_INFO is correctly populated. Lamb uses the following /index.php/some/other type routing, where /some/other should be the PATH_INFO. Instead I want to make setup for a variety of web-servers straightforward, so I've switched to the more robust REQUEST_URI. This simplifies nginx configuration and Caddy and the PHP built-in web-server are compatible.

REQUEST_URI contains everything after the domain name, including the query string, so that needs to be removed:

$request_uri = '/home';
if ( $_SERVER['REQUEST_URI'] !== '/' ) {
    $request_uri = strtok( $_SERVER['REQUEST_URI'], '?' );

We can see that for a request for the root of the site, REQUEST_URI returns / whereas PATH_INFO would be empty, so the code above takes that into account. We can then deduct a router action as follows:

$action = strtok( $request_uri, '/' );

Once the $action is known, it can be checked against an allowed list of actions:

switch ( $action ) {
    case 'edit':

#php #lamb

Sander @ March 18, 2023 at 12:06 am

Godot simplified drag n drop tutorial

I was "following" Generalist Programmer's Godot drag and drop tutorial and I made a few refinements that I wanted to share. Please read the tutorial and then follow along.

Drop-in behaviour

Wouldn't it be cool to give a node drag and drop behaviour simply by dropping in a node with the script, independent of any other scripting?

We can do this by creating a new child node (here called Drag-and-drop Dropin), and attaching the script to it. It must extend from the dropin Node2D.

Node structure

In the script itself we make two changes:

  1. Disconnect the KinematicBody2D's input_event signal, and write it in code in the dropin's _ready function. Call it on it's parent: get_parent().connect("input_event", self, "_on_KinematicBody2D_input_event"). We want to process the input_event of the KinematicBody2D, not the dropin.
  2. In the _process function, assign the mouse position to the parent of the dropin: get_parent().position = Vector2(mousepos.x, mousepos.y).
  3. Same for the last line in the script where we set the position of the parent.

The script then reads as follows:

extends Node2D

var dragging = false

signal dragsignal

func _ready():
    connect("dragsignal", self, "_set_drag_pc")
    get_parent().connect("input_event", self, "_on_KinematicBody2D_input_event")

func _process(delta):
    if dragging:
        var mousepos = get_viewport().get_mouse_position()
        get_parent().position = Vector2(mousepos.x, mousepos.y)

func _set_drag_pc():
    dragging = !dragging

func _on_KinematicBody2D_input_event(viewport, event, shape_idx):
    if event is InputEventMouseButton:
        if event.button_index == BUTTON_LEFT and event.pressed:
        elif event.button_index == BUTTON_LEFT and !event.pressed:
    elif event is InputEventScreenTouch:
        if event.pressed and event.get_index() == 0:
            get_parent().position = event.get_position()

Simplifying the script

I was refactoring this, and it turns out we can simplify this further.

  1. We don't need a dragsignal because the signal is both emitted and consumed within the same script. We can just replace the emit_signal calls with calls to _set_drag_pc(). We can then remove the connect() call in the _ready function, and the signal dragsignal.
  2. The same function is called when the left mouse button is both pressed and not pressed, so we can remove that conditional and remove the elif statement in the input event handler.
  3. As there is only one invocation of _set_drag_pc() we can inline it.

The final script becomes:

extends Node2D

var dragging = false

func _ready():
    get_parent().connect("input_event", self, "_on_KinematicBody2D_input_event")

func _process(delta):
    if dragging:
        var mousepos = get_viewport().get_mouse_position()
        get_parent().position = Vector2(mousepos.x, mousepos.y)

func _on_KinematicBody2D_input_event(viewport, event, shape_idx):
    if event is InputEventMouseButton:
        if event.button_index == BUTTON_LEFT:
            dragging = !dragging
    elif event is InputEventScreenTouch:
        if event.pressed and event.get_index() == 0:
            get_parent().position = event.get_position()

So this dropin can now be added to any node to add Drag-and-Drop behaviour. I think the script can be improved a little so that the node is not centered under the mouse cursor but takes account the offset where it is picked up.

Let me know your thoughts! #godot

Sander @ March 16, 2023 at 4:49 pm

404 Fallback comes to Lamb

I've added a 404 fallback feature to Lamb. What this means is that if you request a URL that doesn't exist on your Lamb instance, it will redirect to the same relative path on the domain you have provided in the configuration, if you enabled this feature.

This means you can move your site from to say and then set that as the 404 fallback url and you will not lose any SEO traffic! Here's an example! #lamb #projects

Sander @ March 15, 2023 at 4:15 pm

Whilst building Lamb I have realised that RedBean ORM, SQLite + plain PHP (used here) or CodeIgniter makes for a very nice setup. Turns out the frameworks just add loads of extra knowledge, that you don't really need if you know not to shoot yourself in the foot with security stuff.

(Famous last words)

Sander @ March 15, 2023 at 12:42 pm

Alright got the webserver configuration figured out and the full site is up. Didn't forget about Caddy. #lamb #projects

Sander @ March 14, 2023 at 5:13 pm

Hello, world! #new_site