AutoHotkey: Resolve Environment Variables in an Input String

As the last little hack session of the year I wanted to build a AutoHotkey function to resolve all occurences of environment variables within an input string.

Normally, this would be pretty straight forward. Use WScript.Shell and call the ExpandEnvironmentStrings function with the input string.

Given that VBScript – and to my understanding WSH – are being deprecated by Microsoft, this solution would run on borrowed time.

Therefore the somewhat more clunky but more “future-proof” method is to rely on locating the %-characters and trying to parse the contents inbetween as environment variables.

We need to be wary of regular %-characters within the string to prevent accidental destruction of string values or false-positives.

So a pure AutoHotkey implementation could look like this:

; Input string
str := "This is 100% true! -- C:\Users\%USERNAME%\test -- %LOCALAPPDATA% -- %"

; Marks the last position of a %-character
lastPos := 0

Loop
{
    ; Find the next environment variable in the string
    pos1 := InStr(str, "%", false, lastPos + 1)
    if !pos1
        break
    pos2 := InStr(str, "%", false, pos1 + 1)
    if !pos2
        break

    ; Extract the environment variable name
    envVar := SubStr(str, pos1 + 1, pos2 - pos1 - 1)
    
    ; Try to get the value of the environment variable
    envValue := EnvGet(envVar)

    ; If the environment variable exists, replace it in the string
    if envValue
        str := SubStr(str, 1, pos1 - 1) . envValue . SubStr(str, pos2 + 1)

    ; Set the last position to the end of the replaced string
    lastPos := pos1
}

; Show the result
MsgBox(str)

This is, however, not the best way to do it. Since Kernel32.dll is loaded automatically in AutoHotkey anyway, using the ExpandEnvironmentStringsW function seems like a much better option:

; Input string
str := "This is 100% true! -- C:\Users\%USERNAME%\test -- %LOCALAPPDATA% -- %"

; Allocate buffer for the expanded string
VarSetStrCapacity(&str2, 1024)

; Call ExpandEnvironmentStringsW to expand environment variables
DllCall("ExpandEnvironmentStringsW", "wstr", str, "wstr", &str2, "uint", 1024)

; Show the result
MsgBox(str2)

Why reinvent the wheel when the platform developer already provides a function for it?

AutoHotkey: Wait for Process to Exit

In today’s small hack session the requirement sounds rather simple: Wait for a given process to exit and then exit the AutohotKey script itself – all while being non-blocking.

The most obvious choice would be a simple loop that checks whether the process still exists. This is also the implementation you will find when searching for this problem on the web. The drawback of this approach is the high cost in runtime, so that is no good; why would I want to burn CPU cycles constantly?

A much better way is to register a callback with Windows and have the operating system notify you once the process has exited.

#Requires AutoHotkey v2.0.2
#SingleInstance Force

DllCall("LoadLibrary", "Str", "Kernel32.dll")

ProcessExitCallback := CallbackCreate(OnProcessExit)

; Function to register a callback for process exit
RegisterForProcessExit(pid) {
    ; Open the process handle with SYNCHRONIZE access
    hProcess := DllCall("OpenProcess", "UInt", 0x00100000, "Int", 0, "UInt", pid, "Ptr")

    if (!hProcess) {
        MsgBox("Failed to open process handle. Error: " DllCall("GetLastError"), "Error opening process handle", "OK IconX")
        return
    }

    ; Create a variable to hold the wait handle
    waitHandle := 0

    ; Register the wait
    success := DllCall("Kernel32.dll\RegisterWaitForSingleObject"
        , "Ptr*", &waitHandle         ; Output wait handle (mutable variable)
        , "Ptr", hProcess            ; The process handle
        , "Ptr", ProcessExitCallback ; The callback function
        , "Ptr", pid                 ; The parameter passed to the callback (PID)
        , "UInt", 0xFFFFFFFF         ; INFINITE wait time
        , "UInt", 0x00000000)        ; WT_EXECUTEONLYONCE

    if (!success) {
        MsgBox("Failed to register wait. Error: " DllCall("GetLastError"), "Error registering callback", "OK IconX")
        DllCall("CloseHandle", "Ptr", hProcess)
        return
    }
}

; The callback function to execute when the process exits
OnProcessExit(param, TimerOrWaitFired) {
    ExitApp(0)
}

; Main script execution
pid := ProcessExist("Notepad.exe") ; Replace with your target process name
if pid
    RegisterForProcessExit(pid)
else
    MsgBox("Notepad.exe is not running!", "Notepad", "OK IconX")

The callback approach has the advantage of being able to run in the background for a long time without noticeable performance impact.

AutoHotkey: Center a Window on the Program’s display

This holiday was all about window management and AutoHotkey for me. Despite the name sounding a lot like AutoIt and being based off of it, AHK is a pretty different beast.

Today’s small hack session is all about centering the active window on the screen the window is on. For that to work, we first have to determine the window’s position and width, then calculate the window X and Y center point, then loop through the displays to get their work area properties and determine which of the displays the window is most likely on.

After that we can calculate the new X and Y positions based on the window dimensions and the determined display work area.

    activeWindow := WinExist("A")
    if !activeWindow
        return

    ; Retrieve the active window's current position and size
    WinGetPos(&winX, &winY, &winWidth, &winHeight, activeWindow)

    ; Calculate the center of the window
    windowCenterX := Round(winX + winWidth / 2, 0)
    windowCenterY := Round(winY + winHeight / 2, 0)

    ; Get number of monitors - but only ones used for the desktop!
    monitorCount := SysGet(80)

    ; Set initial value to check if we found the monitor the window is on...
    monitorIndex := -1

    ; Loop through each monitor to find which one the window is on...
    ; Do this by checking on which monitor the center of the window is, otherwise we get false positives.
    Loop monitorCount {
        MonitorGetWorkArea(A_Index, &workAreaX, &workAreaY, &workAreaWidth, &workAreaHeight)

        if (windowCenterX >= workAreaX and windowCenterX <= workAreaWidth
            and
            windowCenterY >= workAreaY and windowCenterY <= workAreaHeight)
        {
            monitorIndex := A_Index
            break
        }
    }

    ; If still initial value, exit...
    if monitorIndex = -1
        return

    ; Get the dimensions of the determined monitor's work area
    MonitorGetWorkArea(monitorIndex, &workAreaX, &workAreaY, &workAreaWidth, &workAreaHeight)

    ; Calculate new position to center the window
    newX := Round(workAreaX + ((workAreaWidth - workAreaX) - winWidth) / 2, 0)
    newY := Round(workAreaY + ((workAreaHeight - workAreaY) - winHeight) / 2, 0)

    ; Move the window to the new position
    WinMove(newX, newY, , , activeWindow)

This works nicely on multi-monitor setups.

PowerShell: Checking a Font for Specific Glyphs

I had the requirement to check which of the fonts installed on my machine supported Japanese numerals. The easiest way to actually verify this was by doing it through a Powershell script.

The method below only works on Windows machines since it relies on the PresentationCore assembly.

param (
    [switch] $Verbose
)

Add-Type -AssemblyName PresentationCore

# List of Japanese numerals to check
$japaneseNumerals = @(
    [char] 0x4E00, # 一
    [char] 0x4E8C, # 二
    [char] 0x4E09, # 三
    [char] 0x56DB, # 四
    [char] 0x4E94, # 五
    [char] 0x516D, # 六
    [char] 0x4E03, # 七
    [char] 0x516B, # 八
    [char] 0x4E5D, # 九
    [char] 0x5341  # 十
)

# Function to check if a font contains a specific glyph
function FontSupportsCharacter {
    param (
        # The font name to check
        [string] $fontName,
        # The character to check
        [char] $character
    )

    try {
        # Create a GlyphTypeface object for the font
        $typefaces = (New-Object System.Windows.Media.FontFamily($fontName)).GetTypefaces()
        foreach ($typeface in $typefaces) {
            $glyphTypeface = $null
            $typeface.TryGetGlyphTypeface([ref]$glyphTypeface) | Out-Null
            if ($glyphTypeface -and $glyphTypeface.CharacterToGlyphMap.ContainsKey([int][char]$character)) {
                return $true
            }
        }
    } catch {
        # Handle errors, e.g., if the font cannot be loaded
        Write-Verbose -Message "Error loading font: ${fontName}"
    }

    return $false
}

if ($Verbose) {
    $VerbosePreference = "Continue"
}

# Get all installed fonts
$installedFonts = (New-Object System.Drawing.Text.InstalledFontCollection).Families

# Loop through each installed font
foreach ($font in $installedFonts) {
    $glyphFound = $true
    foreach ($numeral in $japaneseNumerals) {
        $exists = FontSupportsCharacter -fontName $font.Name -character $numeral
        if (-not $exists) {
            $glyphFound = $false
            Write-Verbose -Message "Font `"$($font.Name)`" does NOT contain glyph for `"${numeral}`" (U+$([System.String]::Format("{0:X4}", [int][char]$numeral)))."
            break
        }
    }

    if ($glyphFound) {
        Write-Output "Font `"$($font.Name)`" contains all Japanese numerals."
    }
}

Personally, I would expect the operating system to be able to filter fonts, but unfortunately it falls to the user to gain these insights.

tacky-borders enables beautiful, animated borders on Windows

Perhaps you have seen those beautiful, configurable and often animated borders on Hyprland and wished that something similar existed on Windows 10/11.

With tacky-borders‘ upcoming version 1.0 you can get a good number of those features on Windows.

The software can be configured through a yaml file and allows customization of the border thickness, radius, colors, animation and allows application-specific overrides for a number of settings to make sure the borders are properly aligned on applications like Chrome-based browsers.

tacky-borders allows for the “spin” animation, which can be combined with a gradient color setting to create a nice looking RGB crawl around the active window.

Previously I have been using Komorebi’s own implementation, which had a few drawbacks. The border was not as configurable, it was quite pixelated and there was a notable delay when switching between windows. tacky-borders solves all of those issues.

While these features may sound tacky (no pun intended!) to you, having a configurable visual indicator for the active window is incredibly handy in an age where applications render their own titlebars (or lack them altogether). It brings an element of visual clarity, which I highly appreciate.

Why Shiori is still the best Read It Later software

Archiving web contents is important. Not only to have an exact copy of the content from the time you were reading it, but also as a means of preserving the information in an ever volatile world.

In the past I was a huge fan of del.icio.us, a social bookmarking and tagging service. Syncing my bookmarks across different devices felt like the best choice – until I had visited some of the links and found myself in a world of dead links. Ever since then I have been trying to keep an archived copy of the sites around.

Personally, I still think that historious is an absolutely awesome service and I happily pay the subscription fee every year. historious handles more complex sites like reddit or YouTube perfectly and is a great way to quickly archive, tag and search – basically my old del.icio.us workflow but on steroids.

However, this still means you are reliant on someone else to host the service for you. And with the volatile way the web is, historious might just disappear tomorrow. The answer: Host your own application to do the job. But which one?

Over the course of 2023 and 2024 I have evaluated multiple different solutions with very different ideas how to approach the problem of how to archive a website and – more importantly – what to archive from a website.

I always come back to what I personally consider the best software around: Shiori.

While Shiori has some issues with reddit posts, it handles classic forum posts just fine. The content extractor also allows to archive PDF files and news articles from pretty much all the websites I ever visit. Unlike solutions like Obsidian’s Web Clipper that often picks the wrong content, I can be confident that I do not need to double-check the archive of everything I push into Shiori.

Shiori also has a basic Manifest v2 browser extension that allows tagging and saving contents with two clicks – absolutely awesome and all you really need! It does one thing and it does it well.

You have plenty of control to determine on a post-by-post basis whether an archive is available publicly or not and the user interface works great on mobile devices too.

Unlike Readeck or Wallabag, Shiori extracts the contents I want. This might not sound like a big deal to you, but for me it is incredibly important to have a solution I can depend on. Unlike MyBase the contents stays intact when being archived.

Over a long period of time I have come to the realization that for my use-case, Shiori simply seems to be the best fit. The out-of-box experience is just so smooth and stays out of your way. I cannot recommend the software highly enough.

Synology DSM7: “No Health Status”

After cleaning up my NAS I was greeted by a warning after restarting my DS2413+. My pool was in a degraded state, but all drives were fine? Checking the total pool use also did not give me any hints, as I still had plenty of free space.

Turns out none of my drives were reporting any health status anymore (“Health status not available”). Trying to open the S.M.A.R.T. values would just show an error about a failure to communicate with the drive. Those messages popped up for every drive. Did I kill my NAS during cleaning?

Of course not. Manually updating the S.M.A.R.T. database via Diskstation fixed the issue. Somehow the database got corrupted, possibly during an automated update?

Anyway, after updating the database the status turned back to all green. For good measure, I did run a scrub on my pool.

OBS – Websocket Stays Open After Exiting

I have been plagued by this issue for a while: After closing OBS, a zombie process would continue to block the TCP port previously used by OBS’s websocket plugin. Trying to query the process ID never yielded any results. So what gives?

In my case, it turns out that the likely culprit was the OBS Droidcam plugin, which just received an update to mitigate exactly this misbehaviour.

After installing the update I can confirm that the zombie process and the listening TCP port no longer occur.

Why Video Game Consoles are Bad

I recently wondered why my Playstation 3 would turn on and stay in limbo. Turns out that the console was trying to update its firmware to version 4.90 and failed repeatedly.

The reason for all the commotion is error 8002F1F9, which turns out to be a faulty WiFi/Bluetooth board in the console itself.

On a PC, you would utter a sigh and replace the faulty component. On a Playstation 3 Slim you are shit out of luck. The console is now junk because the corresponding components are soldered right onto the motherboard, thus not being user-serviceable.

This is a sorry trend with mass-market electronics. I can get another second-hand Playstation 3 or try Sony’s awful cloud-gaming service, or I will have to dump all my games and use an emulator.

Only one of these routes ensures I can play Blur in 10 years: Emulation.

After Sony removed purchased video content from user libraries and given how fickle and stupid licensing agreements are for cloud-gaming, there is no way I will ever trust Sony with Playstation Now.

Similarly, there is no point in buying another set of ageing hardware, hoping it will last me this time.

Thankfully, my entire Playstation 3 library consists of physical media. Platform holders like Nintendo or Sony have a track record of dropping support for older devices, locking people from accessing their purchased digital content. This is why physical media is the way to go, and going through the process of dumping all your media is a must.

This way, you can play games and watch movies on a PC through emulators. The only digital storefront I have any trust in is Valve’s Steam. In the 20 years I have used Steam, it never failed me once. Meanwhile, Sony has repeatedly screwed me over.

I would also like to address that exclusivity is a cancer. Alan Wake 2 can be the best game of a decade – if it is locked to the Epic Game Store and unavailable on Steam, I do not care for it. Bloodborne is awesome, but I could not care less since it is trapped on a dead platform with shit FPS, forsaken by its creators and publishers.

Developers and publishers should bring their games to the only platform that has shown to be consistently backwards compatible over the last 30 years: The personal computer. Especially since newer generation consoles are essentially neutered PCs anyway.

Because whether you like it or not, your little black box will fail. And when it does, it will lock you out from playing your digital purchases. Not because it is impossible to provide you with proper backwards compatibility on newer systems but because you will begrudgingly buy the same game again and again. Nintendo knows it. Sony knows it.

Increasing SVP-4 stability on 32bit Zoom Player

The SmoothVideo Project is pretty awesome. My only gripe as a passionate user of Zoom Player is that the combination of LAV decoders and the ffdshow raw filter to inject SVP into the pipeline can be pretty unstable. Seeking around in playing media often leads to the media player crashing.

The culprit seems to be ffdshow. While the filter was an absolutely incredible and indispensable part of every afficionados’ filter graph a few years ago, the project is pretty much abandoned. So what are our options to mitigate the crashes without switching to mpv or another 64bit media player?

Turns out that CrendKing has built the awesome AviSynth Filter. After unpacking and installing the filters, we can alter Zoom Player’s Smart Play decoder settings:

Open Zoom Player’s Smart Play options and select a video decoder to test with. In this case, let’s alter the H264 configuration. Click “Configure”.

Select the LAV Video Decoder profile, click “Add Filter” and select the “AviSynth Filter” from the list. Make sure that LAV Video Decoder is the first filter and AviSynth the second.

Afterwards, click “Save active filter(s) as a New Profile”, give it nice name, increase the priority to top and select the profile to activate it. Close the dialogue with OK.

Apply the settings, restart Zoom Player and play a h.264 video file to test. You should see that SVP is active. Seeking in the video should no longer crash the entire player.

Adding Kagi as a Vivaldi Image Search Engine

Kagi, the search engine I was pretty hyped about a year ago, recently added support for loading up on credits via Paypal and other European-friendly, non-credit-card ways.

I have a ton of things to say about the way they flip-flop about their pricing model but that is neither here nor there. Today’s topic is all about adding Kagi to Vivaldi’s list of image search engines.

If you define the following image search URL for Kagi, the reverse image search will work in Vivaldi:

https://kagi.com/images?q={google:imageURL}&reverse=reference

That is some pretty hype stuff because that means I can fully kagi-fy my browser usage.

If you have Kagi’s Chrome extension installed, you will most likely need to uninstall or disable that extension. For some reason, Vivaldi (at the time of writing) ignores the search engine definitions created through the extension.

Launching Remnant 2 with Steam Support through a Batch File

Gunfire Games’ highly anticipated Remnant II is close to launch. If you are not fond of having to start it through Steam’s URL handler or Steam directly, you will quickly notice that simply launching Remnant2.exe leads to the game being offline/in a different mode.

The Unreal Wiki provides the answer as to why this is: The game expects a steam_appid.txt with the corresponding ID of the game (in this case: 1282100) to be present in the main executable’s directory.

You can easily create a Remnant2.bat in steamapps/common/Remnant2 with the following content:

@echo off
set APPID=1282100
echo %APPID% > "%CD%\Remnant2\Binaries\Win64\steam_appid.txt"
start "" "%CD%\Remnant2.exe"

Starting this batch file will automatically create the required steam_appid.txt. The game will consequently use the Steam-specific profile and save directory and launch with full Steam support.

VMware Workstation “hostWin32.c:559” error

When running VMware Workstation 16 or 17 on a Windows host, you perhaps encounter the following error:

PANIC: VERIFY bora\vmx\main\hostWin32.c:559

This error happens when you have a Windows VM running and then try to launch a second Windows VM. As soon as the second machine boots into the desktop/login, it crashes, and the machine stops.

Luckily, this VMware forum post gave me a workaround that solved the issue. Hopefully, it will be helpful for you as well.

I am using an Nvidia graphics adapter and have set the “Max Frame Rate” and “Background Application Max Frame Rate” settings via the Nvidia Control Panel to values other than “Off”. This is what caused the issues for me.

The solution: Simply add a program-specific customization for vmware.exe, ensure both settings are set to “Off” in the customization, and you should be good.

Shape up your Fonts

When I am not busy contemplating suicide, I am a sucker for good-looking, case-appropriate fonts. Especially, when you are staring at code pretty much all day long.

Besides looking good, fonts can also carry additional glyphs and ligatures that can make source code easier to read or render the terminal more beautiful. If you want to see why not every monospace font is equal, you can check out various free and commercial options over at Programming Fonts.

A Font For Code

For programming, I really like MonoLisa. Across Sublime, VS Code and all my JetBrains IDEs, this is the font for editing code. Yes, it might seem pricey, but it also has great customisation options and gives me those nice, wide characters I crave.

Unfortunately, MonoLisa does not have the old DOS-style glyphs for drawing boxes and stuff, so using it on a terminal is not an option for me.

Someone did go through the trouble of creating a Nerd Font patcher specifically for Mono Lisa, which is highly appreciated.

I Command Thee

For the command line, I have found no better font than Gintronic. Yes, it is not the newest font anymore. Mark Frömberg has stated that the font is essentially finished and that he has no plans for ligatures, but that is okay. Gintronic has all the beautiful bits and bops you would expect for a terminal-ready font.

And it can be patched with Nerd Fonts‘ font patcher to add all those nifty icons for Powerline. Cool stuff!

Too Expensive?

“But tsukasa”, I hear you laugh, “Hack is a fine free font! Why would you pay for fonts – twice, even. You big dumb buffoon! JetBrains ships with a perfectly fine monospace font, to boot!”.

And that is a totally fair point. If you are happy with your fonts and see no reason to change them, paying 150-200 EUR per font might seem steep.

I stare at these beautiful characters for about 8 to 10 hours per day, so I need something that is easy on my eyes and pleases my aesthetic senses. Both fonts (or is it font families?) fulfil these requirements and have their own respective strengths. This might seem incredibly nit-picky, but your eyes get hung up if characters are “wrong”. Finding a good font is like finding a good keyboard or pair of shoes – they might all look the same from afar, but the differences become clear when using them.

Plus, I have no issue compensating the fine people making these fonts for their hard work.

A Eulogy for VMware Project Nautilus

Do you remember the hype in 2020 when VMware announced their own OCI-compliant, Docker-compatible container support in their desktop virtualization solutions? I even wrote about it in 2021 after it had some time to mature.

So where is Project Nautilus in 2023, a good three years later? Dead, in the grave.

In 2021, I called vctl “a promising disappointment”, but in 2023 I will have to change that title to “an ambitious stillbirth”. After three whole years and a new major (paid) VMware Workstation version, vctl is still exactly where it was back in 2020.

When people poke VMware staff about this on Twitter, the response pretty much shuts the door on any further development:

I wish I could say it were in a better state. […] We’re taking the time to address this in a more holistic way.

Now, just in case you do not know what “taking the time to address this in a more holistic way” means in plain old English: There are no plans to put another second of development time into it, but marketing says it is a bad look to deprecate it after just one major version, especially since VMware already removed Shared VMs from the desktop virtualization solutions.

This is such a shame. VMware had an admittedly early but incredibly compelling feature in the pipeline that perfectly complemented its core technology and gave developers another reason to love Workstation or Fusion.

Rest in peace, Project Nautilus. I bemoan the waste of potential and awesomeness.