Private/WindowFrame.ps1

# Colouring the Windows Terminal window border through DWM.
#
# Windows Terminal exposes no API to recolour an existing window, but Windows 11
# allows the border colour of any window to be set (DWMWA_BORDER_COLOR, build
# 22000+). The terminal window is located by walking up the parent process chain.
#
# The interop type is compiled on first use only, so it never slows down the
# startup of PowerShell sessions.
#
# Its name must change whenever its members do: .NET cannot redefine a type that
# is already loaded, so a session that reloads an upgraded module would otherwise
# keep the previous shape and fail on the new members.

$script:TcWindowHandle = [IntPtr]::Zero
$script:TcInteropReady = $false
$script:TcInteropFailed = $false

# Looking the window up enumerates every process (a CIM query, ~150 ms). On
# failure we back off, otherwise every directory change would pay that cost for
# nothing on a machine where the window stays unfindable.
$script:TcWindowLookupFailedAt = $null
$script:TcWindowLookupRetrySeconds = 30

$script:TcDwmaBorderColor = 34
$script:TcDwmaCaptionColor = 35
$script:TcDwmColorDefault = [int]-1        # 0xFFFFFFFF

function Initialize-TcInterop {
    [CmdletBinding()]
    param()

    if ($script:TcInteropReady) { return $true }
    if ($script:TcInteropFailed) { return $false }

    if ('TerminalColors.Win32' -as [type]) {
        $script:TcInteropReady = $true
        return $true
    }

    $source = @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
 
namespace TerminalColors
{
    public static class Win32
    {
        [DllImport("dwmapi.dll", PreserveSig = true)]
        private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
 
        private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
 
        [DllImport("user32.dll")]
        private static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
 
        [DllImport("user32.dll")]
        private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
 
        [DllImport("user32.dll")]
        private static extern bool IsWindowVisible(IntPtr hwnd);
 
        [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        private static extern int GetClassName(IntPtr hwnd, StringBuilder buffer, int size);
 
        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        private static extern int GetWindowText(IntPtr hwnd, StringBuilder buffer, int size);
 
        [StructLayout(LayoutKind.Sequential)]
        private struct RECT { public int Left, Top, Right, Bottom; }
 
        [DllImport("user32.dll")]
        private static extern bool GetWindowRect(IntPtr hwnd, out RECT rect);
 
        // Returned as a plain array so the PowerShell side needs no struct type.
        public static int[] GetBounds(IntPtr hwnd)
        {
            RECT r;
            if (!GetWindowRect(hwnd, out r)) { return new int[] { 0, 0, 0, 0 }; }
            return new int[] { r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top };
        }
 
        public static int SetAttributeColor(IntPtr hwnd, int attribute, int colorRef)
        {
            int value = colorRef;
            return DwmSetWindowAttribute(hwnd, attribute, ref value, 4);
        }
 
        public static string GetClass(IntPtr hwnd)
        {
            StringBuilder sb = new StringBuilder(256);
            GetClassName(hwnd, sb, sb.Capacity);
            return sb.ToString();
        }
 
        public static string GetTitle(IntPtr hwnd)
        {
            StringBuilder sb = new StringBuilder(1024);
            GetWindowText(hwnd, sb, sb.Capacity);
            return sb.ToString();
        }
 
        public static IntPtr[] GetProcessWindows(uint processId)
        {
            List<IntPtr> result = new List<IntPtr>();
            EnumWindows(delegate(IntPtr hwnd, IntPtr lParam)
            {
                uint pid;
                GetWindowThreadProcessId(hwnd, out pid);
                if (pid == processId && IsWindowVisible(hwnd))
                {
                    result.Add(hwnd);
                }
                return true;
            }, IntPtr.Zero);
            return result.ToArray();
        }
    }
}
'@


    try {
        Add-Type -TypeDefinition $source -ErrorAction Stop
        $script:TcInteropReady = $true
        return $true
    } catch {
        Write-Verbose "TerminalColors: interop unavailable ($($_.Exception.Message))"
        $script:TcInteropFailed = $true
        return $false
    }
}

function Get-TcParentProcessMap {
    <#
        .SYNOPSIS
        PID -> @{ Parent; Name } table, built with a single CIM query.
    #>

    [CmdletBinding()]
    param()

    $map = @{}
    try {
        $processes = Get-CimInstance -ClassName Win32_Process -Property ProcessId, ParentProcessId, Name -ErrorAction Stop
    } catch {
        try {
            $processes = Get-WmiObject -Class Win32_Process -Property ProcessId, ParentProcessId, Name -ErrorAction Stop
        } catch {
            Write-Verbose "TerminalColors: could not enumerate processes ($($_.Exception.Message))"
            return $map
        }
    }

    foreach ($p in $processes) {
        $map[[int]$p.ProcessId] = @{ Parent = [int]$p.ParentProcessId; Name = [string]$p.Name }
    }
    return $map
}

function Select-TcTerminalWindow {
    <#
        .SYNOPSIS
        Picks the real terminal window among the candidates of a Windows Terminal
        process. Returns the handle, or $null.
 
        .DESCRIPTION
        A single Windows Terminal process exposes several windows of class
        CASCADIA_HOSTING_WINDOW_CLASS, all carrying the title of the active tab:
        the real ones, plus hidden placeholders measured at 160x28 and parked at
        -32000,-32000. Colouring a placeholder's border succeeds - DWM returns
        S_OK - and changes nothing on screen, which is why the border used to
        appear only now and then.
 
        So geometry comes first: anything too small to be a terminal, or parked
        off-screen, is discarded. The title only arbitrates between real windows,
        and the largest one settles any remaining tie.
 
        .PARAMETER Candidates
        Objects carrying Handle, Left, Top, Width, Height and Title.
 
        .PARAMETER Title
        Title of this session's tab. Windows Terminal mirrors the active tab's
        title onto its window, so it identifies our window among several.
    #>

    [CmdletBinding()]
    param(
        [object[]] $Candidates,
        [string] $Title
    )

    if ($null -eq $Candidates) { return $null }

    # -32000 is where Windows parks minimised and placeholder windows.
    $real = @($Candidates | Where-Object {
        $_.Width -ge 200 -and $_.Height -ge 100 -and $_.Left -gt -30000 -and $_.Top -gt -30000
    })
    if ($real.Count -eq 0) { return $null }
    if ($real.Count -eq 1) { return $real[0].Handle }

    if (-not [string]::IsNullOrWhiteSpace($Title)) {
        $matched = @($real | Where-Object { $_.Title -eq $Title })
        if ($matched.Count -eq 1) { return $matched[0].Handle }
        if ($matched.Count -gt 1) { $real = $matched }
    }

    return (@($real | Sort-Object -Property { [long]$_.Width * [long]$_.Height } -Descending)[0]).Handle
}

function Get-TcTerminalWindowHandle {
    <#
        .SYNOPSIS
        Handle of the Windows Terminal window hosting the current session. The
        result is cached for the lifetime of the session.
    #>

    [CmdletBinding()]
    param([switch] $Refresh)

    if (-not $Refresh -and $script:TcWindowHandle -ne [IntPtr]::Zero) { return $script:TcWindowHandle }
    if (-not (Test-TcWindowsTerminal)) { return [IntPtr]::Zero }

    if (-not $Refresh -and $script:TcWindowLookupFailedAt) {
        if (((Get-Date) - $script:TcWindowLookupFailedAt).TotalSeconds -lt $script:TcWindowLookupRetrySeconds) {
            return [IntPtr]::Zero
        }
    }

    if (-not (Initialize-TcInterop)) {
        $script:TcWindowLookupFailedAt = Get-Date
        return [IntPtr]::Zero
    }

    # 1. Walk up the parent chain to WindowsTerminal.exe
    $map = Get-TcParentProcessMap
    $terminalPid = 0
    $current = $PID
    for ($depth = 0; $depth -lt 12; $depth++) {
        if (-not $map.ContainsKey($current)) { break }
        $entry = $map[$current]
        if ($entry.Name -match '^WindowsTerminal(Preview)?\.exe$') { $terminalPid = $current; break }
        if ($entry.Parent -le 0 -or $entry.Parent -eq $current) { break }
        $current = $entry.Parent
    }

    # 2. Fallback: a single Windows Terminal process is running
    if ($terminalPid -eq 0) {
        $candidates = @(Get-Process -Name 'WindowsTerminal', 'WindowsTerminalPreview' -ErrorAction SilentlyContinue)
        if ($candidates.Count -eq 1) { $terminalPid = $candidates[0].Id }
    }

    if ($terminalPid -eq 0) {
        Write-Verbose 'TerminalColors: Windows Terminal process not found.'
        $script:TcWindowLookupFailedAt = Get-Date
        return [IntPtr]::Zero
    }

    # Windows Terminal can host several windows in a single process, and it also
    # keeps hidden placeholders of the same class carrying the same title. Collect
    # their geometry so the real one can be told apart.
    # The @() is required: a single-result pipeline does not return an array.
    $candidates = @([TerminalColors.Win32]::GetProcessWindows([uint32]$terminalPid) |
        Where-Object { [TerminalColors.Win32]::GetClass($_) -eq 'CASCADIA_HOSTING_WINDOW_CLASS' } |
        ForEach-Object {
            $bounds = [TerminalColors.Win32]::GetBounds($_)
            [pscustomobject]@{
                Handle = $_
                Left   = [int]$bounds[0]
                Top    = [int]$bounds[1]
                Width  = [int]$bounds[2]
                Height = [int]$bounds[3]
                Title  = [TerminalColors.Win32]::GetTitle($_)
            }
        })

    $handle = Select-TcTerminalWindow -Candidates $candidates -Title (Get-TcWindowTitle)
    if ($null -eq $handle) {
        $script:TcWindowLookupFailedAt = Get-Date
        return [IntPtr]::Zero
    }

    $script:TcWindowHandle = $handle
    $script:TcWindowLookupFailedAt = $null
    return $handle
}

function Test-TcActiveTab {
    <#
        .SYNOPSIS
        Tells whether this session's tab is the one the window is showing.
        Returns $true, $false, or $null when it cannot be determined.
 
        .DESCRIPTION
        The border belongs to the window, not to the tab, so only the visible tab
        may drive it. Without this check every tab writes the border on its first
        prompt - and since those prompts race each other while a window opens, a
        colourless tab resetting the border can win, leaving a grey border on a
        window whose visible tab belongs to a project.
 
        Windows Terminal mirrors the active tab's title onto the window title, and
        we set our own tab title, so comparing the two answers the question. When
        the title is not usable ([-NoTitle], [suppressApplicationTitle], window not
        located) the answer is $null and the caller keeps its previous behaviour
        rather than silently dropping the border.
    #>

    [CmdletBinding()]
    param()

    # Our own console title, plus the titles we set just before it: Windows
    # Terminal propagates a title change to the window asynchronously.
    $candidates = @(@(Get-TcWindowTitle) + @($script:TcAppliedTitles))
    if (@($candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count -eq 0) { return $null }

    $hwnd = Get-TcTerminalWindowHandle
    if ($hwnd -eq [IntPtr]::Zero) { return $null }

    $windowTitle = ''
    try { $windowTitle = [TerminalColors.Win32]::GetTitle($hwnd) } catch { return $null }

    return (Test-TcTitleMatchesWindow -WindowTitle $windowTitle -Candidates $candidates)
}

function Test-TcTitleMatchesWindow {
    <#
        .SYNOPSIS
        Decision behind Test-TcActiveTab, kept separate so it can be reasoned
        about and tested on its own.
 
        Returns $null when the window title tells us nothing.
    #>

    [CmdletBinding()]
    param(
        [string] $WindowTitle,
        [string[]] $Candidates
    )

    if ([string]::IsNullOrWhiteSpace($WindowTitle)) { return $null }

    $usable = @($Candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    if ($usable.Count -eq 0) { return $null }

    return ($usable -contains $WindowTitle)
}

function Set-TcWindowBorderColor {
    <#
        .SYNOPSIS
        Colours the terminal window's border. Returns $true on success
        (HRESULT S_OK).
    #>

    [CmdletBinding()]
    param(
        [hashtable] $Rgb,
        [switch] $IncludeCaption
    )

    $hwnd = Get-TcTerminalWindowHandle
    if ($hwnd -eq [IntPtr]::Zero) { return $false }

    # COLORREF = 0x00BBGGRR
    $colorRef = ([int]$Rgb.R) -bor (([int]$Rgb.G) -shl 8) -bor (([int]$Rgb.B) -shl 16)

    $hr = [TerminalColors.Win32]::SetAttributeColor($hwnd, $script:TcDwmaBorderColor, $colorRef)
    if ($IncludeCaption) {
        [void][TerminalColors.Win32]::SetAttributeColor($hwnd, $script:TcDwmaCaptionColor, $colorRef)
    }

    if ($hr -ne 0) {
        Write-Verbose ('TerminalColors: DwmSetWindowAttribute returned 0x{0:X8}' -f $hr)
        return $false
    }
    return $true
}

function Reset-TcWindowBorderColor {
    [CmdletBinding()]
    param([switch] $IncludeCaption)

    if (-not $script:TcInteropReady -and -not (Test-TcWindowsTerminal)) { return $false }
    $hwnd = Get-TcTerminalWindowHandle
    if ($hwnd -eq [IntPtr]::Zero) { return $false }

    $hr = [TerminalColors.Win32]::SetAttributeColor($hwnd, $script:TcDwmaBorderColor, $script:TcDwmColorDefault)
    if ($IncludeCaption) {
        [void][TerminalColors.Win32]::SetAttributeColor($hwnd, $script:TcDwmaCaptionColor, $script:TcDwmColorDefault)
    }
    return ($hr -eq 0)
}