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.

Streamlining your OBS workflow

Building a stream layout is a lot of work. Design elements like colours, fonts and layouts have to be consistent. In the past, I used to design things in Photoshop or Affinity Photo, cut the assets up into smaller pieces and then either use them in OBS directly or run them through DaVinci Resolve for some basic animation. This approach works fine on a rather static layout.

Now I’ve been toying around with the idea of what I call “After Dark” streams that have their own, slightly different style. The fonts and layouts stay the same, however, all the colours change. With my old workflow I would either need to re-export and edit all the assets… or find another way.

For a while, I have been doing my layouts as HTML documents now. Using CSS animations and jQuery as a base for dynamic data processing, I can easily switch things around.

Since I am on Windows, reading/writing the contents of a JSON file is really easy with Powershell. So I can map some Stream Deck keys to perform value toggles in the JSON, causing my layout to dynamically adjust.

Same for the “Now Playing on Pretzel” widget. It processes the JSON file generated by Pretzel’s desktop client, dynamically resizes the widget and even fades out once music stops playing.

HTML stream layout comparison

The overall advantage is obvious: If I ever choose to edit the colour scheme, it is one edit within one CSS file. New font? A couple of changes. Changing the stream title, metadata et al is also just a simple set of nodes in a JSON file – the rest of the layout dynamically adjusts. And it is all easily accessible through one press on my Stream Deck.

Additionally, this approach reduces the number of required scenes/elements drastically. Whereas you would either need to toggle the visibility of sources or duplicate scenes on a more traditional setup, everything runs in proper code here. I have no dedicated intermission scene… the title card simply transforms into it, keeping all elements coherent within the scene.

“But Tsukasa, the performance impact”, people will yell. I dare say that any blur effect on a fullscreen video in OBS has probably a heavier impact on the performance than a reusable browser source. The entire title card sits at around 10% CPU usage, with a good portion of that going towards the VLC video source.

Dynamic changes to the layout

So I feel it is high time people stop using video-based layouts and migrate to proper HTML-based ones.