Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Run thumbnailer for arbitrary paths

For some reason, it's complex to tell a thumbnailer program to index arbitrary paths, such as a single directory or even single file. I don't relish using dbus, but begrudgingly use it to accomplish some goals. I'm pragmatic, to a degree.

Apparently tools like tumbler need dbus to receive messages about what paths to index.

So I vibecoded some syntax for dbus commands and some filetypes for a find operation, and wrote some functions that I find useful.

files/2026/listings/thumbnail_functions.sh (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#!/bin/sh
# File: thumbnail_functions.sh
# Location: https://bgstack15.ddns.net/blog/posts/2026/08/30/run-thumbnailer-for-arbitrary-paths/
# Author: bgstack15
# Startdate: 2026-08-17-2 19:29
# Title: Functions for Thumbnail Operations
# Purpose: Store all thumbnail operations in one location
# History:
# Usage:
#    source this file, and call the functions you want
#    . thumbnail_functions.sh
#    thumbnail_dir ~/Downloads
# Reference:
#    man busctl
# Improve:
# Dependencies:
#    A thumbnailer implementation, perhaps tumbler
# Documentation:
#    Running these commands in other windows:
#       busctl --user monitor org.freedesktop.thumbnails.Thumbnailer1
#       G_MESSAGES_PREFIXED= G_MESSAGES_DEBUG=all /usr/lib/x86_64-linux-gnu/tumbler-1/tumblerd
#    Bare command:
#       busctl --user call org.freedesktop.thumbnails.Thumbnailer1 /org/freedesktop/thumbnails/Thumbnailer1 org.freedesktop.thumbnails.Thumbnailer1 Queue asasssu 2 file:///home/bgstack15/Downloads/godzilla.jpg file:///home/bgstack15/Downloads/mary-had-a-little-lamb.jpg 2 "image/jpeg" "image/jpeg" "xx-large" "" 0
#       gdbus call --session --dest org.freedesktop.thumbnails.Thumbnailer1 --object-path /org/freedesktop/thumbnails/Thumbnailer1 --method org.freedesktop.thumbnails.Thumbnailer1.Queue "['file:///home/bgstack15/Downloads/godzilla.jpg']" "['image/jpeg']" "normal" "[]" "0"
#       WARNING: dbus-send does not correctly handle commas in the filenames. I was unable to discover how to escape the comma.
#       dbus-send --dest=org.freedesktop.thumbnails.Thumbnailer1 /org/freedesktop/thumbnails/Thumbnailer1 org.freedesktop.thumbnails.Thumbnailer1.Queue array:string:"file:///path/to/your/file.jpg" array:string:"image/jpeg" string:"normal" string:"leave-on-disk" uint32:0

thumbnail_dir() {
    _dir="${DIR:-${1:-.}}"
    _size="${THUMBNAIL_SIZE:-${2:-normal}}"
    _recursive="${RECURSIVE}"
    _max_depth="${MAX_DEPTH:-1}"
    if test -z "${_recursive}" ;
    then
        if echo " ${@} " | grep -qiE -e ' -r | --recursive ' 1>/dev/null 2>&1 ;
        then
            # safety max depth.
            _max_depth=10
        fi
    fi
    _infiles="$(
        find "${_dir}" -mindepth 1 -maxdepth "${_max_depth}" -type f \( \
            -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' -o -iname '*.gif' \
            -o -iname '*.bmp' -o -iname '*.tif' -o -iname '*.tiff' -o -iname '*.svg' -o -iname '*.avif' \
            -o -iname '*.heic' -o -iname '*.ico' -o -iname '*.jp2' -o -iname '*.jxl' -o -iname '*.apng' \
            \) -print0 \
        | sed -r -e 's/ /%20/g;' | xargs -0 -I{} printf '%s ' "{}"
    )"

    _uris="$(
        for _word in ${_infiles} ; do
            printf '"file://%s" ' "${_word}"
        done
    )"

    _mimetypes="$(
        for _word in ${_infiles} ; do
            _n="$( echo "${_word}" | sed -r -e 's/%20/ /g;' )"
            file -b --mime-type "${_n}" | xargs -I{} printf '%s ' "{}"
        done
    )"

    _len="$( printf '%s\n' "${_infiles}" | wc -w )"
    eval busctl --user call org.freedesktop.thumbnails.Thumbnailer1 /org/freedesktop/thumbnails/Thumbnailer1 \
        org.freedesktop.thumbnails.Thumbnailer1 Queue \
        "asasssu" \
        "${_len}" "${_uris}" \
        "${_len}" "${_mimetypes}" \
        "${_size}" \
        "\"\"" \
        0
}

thumbnail_file() {
    _file="${FILE:-${1}}"
    _size="${THUMBNAIL_SIZE:-${2:-normal}}"
    _mimetype="$( file -b --mime-type "${_file}" 2>/dev/null )"
    _file="$( echo "${_file}" | sed -r -e 's/ /%20/g;' )"
    busctl --user call org.freedesktop.thumbnails.Thumbnailer1 /org/freedesktop/thumbnails/Thumbnailer1 \
        org.freedesktop.thumbnails.Thumbnailer1 Queue \
        "asasssu" \
        "1" "file://${_file}" \
        "1" "${_mimetype}" \
        "${_size}" \
        "" \
        0
}

So now I can run the following, to force the thumbnailer to interpret a directory.

. ./thumbnail_functions.sh
thumbnail_dir ~/Downloads

mcl_villager_cheats now includes a "reload trades" button

I was inspired by some recent conversations about an upcoming new Luanti mod about randomizing the trades a villager offers, and I wrote my own implementation! It involved copying the entirety of the villager code from Mineclonia (ContentDB) and adding a small patch. Well, and the logic for changing the trades too.

The concept of rotating trades of a villager can be done in-game, for a villager who has never traded, by removing his job block and replacing it. This button short-circuits that. And my implementation leaves it possible even after the villager has participated in a trade. So yes, it's cheating, but that's in the name of the mod. I don't even have a configuration setting for disabling it if the villager xp > 0 (that is, he has traded at all). I could add it, if anybody is interested.

files/2026/listings/diff-villager.patch (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Date: 2026-08-11
Message: The difference between Mineclonia villager.lua and mcl_villager_cheats villager.lua
Last-Version: 0.123.0
Author: bgstack15
Command: diff -aur ~/.minetest/games/mineclonia/mods/ENTITIES/mobs_mc/villager.lua ~/.minetest/mods/mcl_villager_cheats/villager.lua
--- .minetest/games/mineclonia/mods/ENTITIES/mobs_mc/villager.lua	2026-07-26 14:20:37.152618130 -0400
+++ .minetest/mods/mcl_villager_cheats/villager.lua	                2026-08-11 11:52:24.416485282 -0400
@@ -711,6 +711,12 @@
  list[current_player;main;3.97,7.98;9,1;]
 
 ]]
+-- stackrpms,6
+fs_footer_template = fs_footer_template ..
+	"image_button[4.0,0.2;1.0,1.0;mcl_villager_cheats_reload.png;reload;]" ..
+	"tooltip[reload;Randomize available trades" ..
+	core.colorize("#47f424","\n(" .. core.get_current_modname() .. ")") ..
+	";#000000;#ffffff]"
 
 -- arg 1 = %s = wanted
 -- arg 2 = %s = wanted tooltip
@@ -903,6 +909,15 @@
 
 core.register_on_player_receive_fields (function (player, formname, fields)
 	if formname == "mobs_mc:trading_formspec" then
+		--stackrpms,9
+		if fields.reload then
+			local trader = trading_players[player]
+			if trader and is_valid (trader) then
+				mcl_villager_cheats.reload_trades(trader)
+				local entity = trader:get_luaentity ()
+				entity:reload_trades()
+			end
+		end
 		if fields.quit then
 			return_fields (player)
 			local trader = trading_players[player]
@@ -942,6 +957,11 @@
 		type = "detached",
 		name = inv_name,
 	})
+	-- stackrpms,5 Need to remove the previous inventory, so this new inventory
+	if inv then
+		core.remove_detached_inventory(inv_name)
+		inv = nil
+	end
 	if not inv then
 		inv = core.create_detached_inventory (inv_name, inv_class,
 							  playername)
@@ -6783,4 +6803,5 @@
 -- Villager spawning.
 ------------------------------------------------------------------------
 
-mcl_mobs.register_egg ("mobs_mc:villager", S("Villager"), "#563d33", "#bc8b72", 0)
+-- stackrpms,2 Do not run this again
+--mcl_mobs.register_egg ("mobs_mc:villager", S("Villager"), "#563d33", "#bc8b72", 0)

Image

Mineclonia in Devuan Ceres needs SDL2

In Devuan Ceres (unstable), I had a problem with Mineclonia (my build of Luanti). Shift+left click would not move items in the inventory screens. A shift+left click normally moves the entire stack (to whichever destination). Shift+middle click still works. So somehow it's just shift+left click.

I was not the first to discover this bug.

Come to find out, in Devuan unstable, libsdl2-2.0-0 version 2.32.70+ds-1 is a sdl2-compat layer for sdl3. That is, it uses SDL3 but with the bindings for sdl2 so an application can use it like it uses sdl2.

Apparently I wanted to install libsdl2-classic which is real SDL2. And then I need to add this to my invocation of my game:

SDL_DYNAMIC_API='/usr/$LIB/sdl2-classic/libSDL2-2.0.so.0' mineclonia

I can live with that. I'm sure libsdl2-classic will go away eventually, but then also eventually hopefully they'll fix their bug in SDL3.

AoE2DE CaptureAge on Linux for 2026

I ended up having to use a fuller script from TeknoHamster.

I also had to do some crazy tricks with protontricks or winetricks to run protontrikcs 813780 d3dcompiler_47. I think what ended up working was adjusting the script to run a winetricks command in the WINEPREFIX.

files/2026/listings/captureage.sh (Source)

#!/usr/bin/env bash
# Taken directly from https://gist.githubusercontent.com/TeknoHamster/d8b2f90a1cd618074aa1e35c81e7bc4b/raw/972832a3e37ef32d255804d19aae99550ec9fb26/captureage from https://gist.github.com/TeknoHamster/d8b2f90a1cd618074aa1e35c81e7bc4b
APPID=813780
STEAM_PATH="${STEAM_PATH:-$HOME/.local/share/Steam}"
PROTON_NAME="${PROTON_NAME:-Proton - Experimental}"
export PROTON_LOG=1
export STEAMAPPS="$STEAM_PATH/steamapps"
export STEAM_COMPAT_DATA_PATH="$STEAMAPPS/compatdata/$APPID"
export STEAM_COMPAT_CLIENT_INSTALL_PATH="$STEAM_PATH"
export WINEPREFIX="$STEAM_COMPAT_DATA_PATH/pfx"
export GAME_DIR="$STEAMAPPS/common/AoE2DE"
export GAME_DIR_WIN='S:\common\AoE2DE'
export CAPTUREAGE_STATE="$WINEPREFIX/drive_c/users/steamuser/AppData/Roaming/CaptureAge/persistedState_prod.json"
export CAPTUREAGE_EXE_WIN='C:\users\steamuser\AppData\Local\Programs\CaptureAge\CaptureAge.exe'
export CAPTUREAGE_DIR_WIN='C:\users\steamuser\AppData\Local\Programs\CaptureAge'
export STEAM_COMPAT_INSTALL_PATH="$GAME_DIR"
export STEAM_COMPAT_LIBRARY_PATHS="$STEAMAPPS"
export PROTON_SET_GAME_DRIVE=1
# Adjust the path to the proton executable used by Age of Empires II
export PROTON_EXEC="${PROTON_EXEC:-$STEAMAPPS/common/$PROTON_NAME/proton}"
# export WINEDEBUG=warn+all # This is for debug logs
# export OBS_VKCAPTURE=1 # Enable this for OBS Videogame capture
prepare_prefix() {
  local game_dir_win
  local game_dir_escaped
  game_dir_win="$GAME_DIR_WIN"
  game_dir_escaped="${game_dir_win//\\/\\\\}"
  mkdir -p "$WINEPREFIX/dosdevices"
  ln -sfn "$STEAMAPPS" "$WINEPREFIX/dosdevices/s:"
  if [ -f "$WINEPREFIX/user.reg" ]; then
    GAME_DIR_ESCAPED="$game_dir_escaped" perl -0pi -e 'BEGIN{$r=$ENV{GAME_DIR_ESCAPED}} s#"Install Location"="[^"]*"#"Install Location"="$r"#g' "$WINEPREFIX/user.reg"
  fi
  if [ -f "$CAPTUREAGE_STATE" ]; then
    tmp_state="$(mktemp)"
    jq --arg dir "$game_dir_win" '.lastUsedGameDirectory = $dir' "$CAPTUREAGE_STATE" > "$tmp_state" && mv "$tmp_state" "$CAPTUREAGE_STATE"
  fi
}
reg_add() {
  "$PROTON_EXEC" run reg add "$@"
}
#This creates Windows registries to make the Spectate with CA button work
register_captureage() {
  local protocol_command
  protocol_command="\"$CAPTUREAGE_EXE_WIN\" \"%1\""
  for root in HKCU\\Software\\Classes HKLM\\Software\\Classes; do
    reg_add "$root\\captureage" /ve /d "URL:captureage" /f
    reg_add "$root\\captureage" /v "URL Protocol" /d "" /f
    reg_add "$root\\captureage\\shell\\open\\command" /ve /d "$protocol_command" /f
  done
  for root in HKCU HKLM; do
    reg_add "$root\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\CaptureAge.exe" /ve /d "$CAPTUREAGE_EXE_WIN" /f
    reg_add "$root\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\CaptureAge.exe" /v "Path" /d "$CAPTUREAGE_DIR_WIN" /f
  done
}
#install capture age (only needed once). Adjust the location to point to the installer you downloaded.
#"$PROTON_EXEC" run "$PWD/CaptureAgeSetup.exe"
# run capture age
# Older version of CA:DE were installed in AppData/Local/Programs/CaptureAge . If it doesn't launch for you, check this path
prepare_prefix
register_captureage
"$PROTON_EXEC" run "C:/users/steamuser/AppData/Local/Programs/CaptureAge/CaptureAge.exe"

onfoss.org from browser history

I was on a travel laptop and I was looking through my browser history. I found this really awesome site I didn't recall visiting: onfoss.org. This is some group of people who get together to virtual LAN party!

This is really fascinating, that I didn't remember this at all! This place links to so many free and libre software titles. I've only heard of some of them.

I already play some of them.

I've heard of a few that I haven't played.

And loads I haven't heard of before!

There's so many here that I hadn't heard of before! Most of them are first-person shooters, and I play only a couple of those. But they're all FLOSS, which is very important!

Idea: use protonup for Lego Universe

I have not formalized this process, but I tested it at least once: you can use protonup to get a Wine (Proton) environment for running Lego Universe.

I use Devuan GNU+Linux, release Ceres (unstable) which is based on Debian Sid (unstable), and the current package for wine is version 10.0~repack-12, but 10.0~repack-6 is the last wine package that runs Lego Universe client. I suspect it is an oversight, related to the combining of 32-bit and 64-bit/wow64 stuff I don't understand, and people in Debian unstable don't seem to test or care about.

If you want to fetch 10.0~repack-6, set this in file /etc/apt/sources.list.d/snapshot.list:

# 2025-11-02-1 16:05 Fix DLU Lego Universe client
deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/20251001T203719Z/ unstable main

So for over half a year now I've had to pin an old version of wine, albeit the same upstream version string but a different repack in my Linux distro. I dislike having to pin to old versions of things (that are too big for me to compile myself).

If I ever need to upgrade wine to current (and they haven't fixed the 32-bit/64-bit whatever), I could use protonup to easily fetch GE-Proton.

To install protonup, I had to use a virtual environment.

python3 -m venv ~/venv1
. ~/venv1/bin/activate
pip install protonup
protonup --releases | tail # find highest version
protonup -t GE-Proton11-1

It will prompt to confirm the installation, and also print the deployed directory.

[INFO] Installed in: /home/bgstack15/.steam/root/compatibilitytools.d/GE-Proton11-1

Use that for PROTONPREFIX or WINEPREFIX when running an application.

Image

luanti inventory pouches update from upstream

I have some small fixes for Luanti mod inventory_pouches (ContentDB). I have sent them to upstream when I wrote them, but they have not been absorbed yet.

Upstream had a new commit that fixes color dyeing! It's pretty great. So I didn't need my color fixes anymore, and my remaining patches applied cleanly.

  • persist inventory across server restarts
  • load pouches when player logs in (affects inventory_icon mod)
  • fix pouch inventory alignment (graphical only, MCL)

I wrote a new patch also, to convert the old color system to the new color system, when the player opens the pouch. So as long as the player has used the pouch once, then he can re-dye the pouch in the expected fashion if desired.

I also needed to patch inventory_icon to read the new meta attribute that stores the color.

Screenshot showing dyeing a pouch in Mineclonia
Image

OS Updates notes, July 2026

The postinst script for GNU Screen, that is, the dpkg component that runs after a package gets installed/configured, now depends on functionality provided by systemd-tmpfiles.

Snippet from /var/lib/dpkg/info/screen.postinst

# Automatically added by dh_installtmpfiles/14.1
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
    systemd-tmpfiles ${DPKG_ROOT:+--root="$DPKG_ROOT"} --create screen-cleanup.conf
fi

It doesn't give you the option to ignore errors. I assume it's generated from https://salsa.debian.org/debian/screen/-/blob/master/debian/screen.screen-cleanup.tmpfiles?ref_type=heads

LeePen suggested that I try seedfiles, which I did on a few systems. Both the standalone-tmpfiles and seedfiles worked fine, to let screen install correctly.