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.