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.

Content-based file search with Powershell and FileLocator

I love Powershell. Unfortunately, as soon as we cross into the realm of trying to grep for a specific string in gigabytes worth of large files, Powershell becomes a bit of a slowpoke.

Thankfully I also use the incredible FileLocator Pro, a highly optimized tool for searching file contents – no matter the size. The search is blazingly fast – and you can easily utilize FileLocator’s magic within Powershell!

For the sake of clarity: I will be using Powershell 7.1.3 for the following example.

# Add the required assembly
Add-Type -Path "C:\Program Files\Mythicsoft\FileLocator Pro\Mythicsoft.Search.Core.dll"

# Prepare the base search engine and criteria
$searchEngine                      = New-Object Mythicsoft.Search.Core.SearchEngine
$searchCriteria                    = New-Object Mythicsoft.Search.Core.SearchFileSystemCriteria

$searchCriteria.FileName           = "*.log"
$searchCriteria.FileNameExprType   = [Mythicsoft.Search.Core.ExpressionType]::Boolean

$searchCriteria.LookIn             = "C:\Temp\LogData"
$searchCriteria.LookInExprType     = [Mythicsoft.Search.Core.ExpressionType]::Boolean

$searchCriteria.SearchSubDirectory = $true

$searchCriteria.ContainingText     = ".*The device cannot perform the requested procedure.*"
$searchCriteria.ContentsExprType   = [Mythicsoft.Search.Core.ExpressionType]::RegExp

# Actually perform the search, $false executes it on the same thread as the Powershell session (as in: it's blocking)
$searchEngine.Start($searchCriteria, $false)

foreach($result in $searchEngine.SearchResultItems)
{
   # SeachResultItems are on a per-file basis.
   foreach($line in $result.FoundLines)
   {
      "Match in $($result.FileName) on line $($line.LineNumber): $($line.Value)"
   }
}

Wowzers, that’s pretty easy! In fact, a lot easier (and quicker, to boot!) than playing around with Get-Contents, StreamReaders and the like.

One thing of note here: Between running this on a loop for every file in a directory, it is actually quicker to process an entire tree of folders/files. The larger the dataset, the larger the gains through invoking FileLocator.

And yeah, you can use FileLocator on the command line through flpsearch.exe – however the results are not as easily digestable as the IEnumerables you get through the assembly.

ZNC Playback script for mIRC/AdiIRC

The main reason I use an IRC bouncer is so I can detach from the bouncer and get the messages I missed the next time I attach to it again. ZNC provides support for this feature by default, however, there is a third-party module called Playback that has some additional bells and whistles.

To properly utilize the Playback module, you need to adjust two settings on your bouncer and your IRC client needs to do some minor lifting. After searching the internet far and wide, I have not come across a premade AdiIRC script that worked the way I wanted it to, so I figured it was high time to improve the situation.

So what do we actually need to teach our IRC client? Essentially, the client needs to keep track of when it received the network’s last message, so it can request all newer messages that are newer than this timestamp from the bouncer upon reconnect. Sounds easy enough, especially since there were some example scripts for other clients linked on the wiki page for Playback.

I wired up a basic mIRC/AdiIRC script that will retain timestamps of ZNC connections on a per-network basis. Instead of merely updating the timestamp when a PRIVMSG comes in, the script also updates the timestamp on JOIN/PART events to cover “quiet” channels/networks.

To avoid the odd timezone problems, the script will read the timestamp from IRCv3 enabled timestamp parts within events/messages. I still have some odd timezone issues between my own IRCd, bouncer and client, but this is likely due to a configuration problem on my end. On the major networks, the script operates as intended. The data is held in a small hashtable that gets serialized/deserialized to an INI file on exit/startup.