Private/Win32.ps1

# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Tyler Vigario

<#
  The P/Invoke surface for window discovery and visibility.

  The type name carries a revision suffix on purpose. A long-lived shell that has
  already imported an older Greenroom holds the previous type, and Add-Type cannot
  redefine a type in a live session -- so a fixed name plus a "load once" guard
  silently binds to the stale definition and fails on any member added since. Bump
  the suffix whenever the member list changes.

  Being inside a module does not change that: Remove-Module unloads PowerShell
  functions, not a loaded .NET assembly.
#>


if (-not ('Greenroom.Win1' -as [type])) {
    Add-Type -Namespace Greenroom -Name Win1 -MemberDefinition @'
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr p);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int GetClassName(IntPtr h, System.Text.StringBuilder s, int n);
[DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int GetWindowText(IntPtr h, System.Text.StringBuilder s, int n);
'@

}

# Module-scoped rather than global. In bin/greenroom.ps1 these were script-scoped in
# a script that was the whole world; here the module scope is a real boundary, so
# they cannot collide with a caller's variables.
$script:SW_HIDE    = 0
$script:SW_RESTORE = 9

# Where an instance keeps its config, window record and logs.
function Get-GreenroomStateRoot {
    Join-Path $env:USERPROFILE '.claude\greenroom'
}

<#
  Whether a window is currently visible.

  A one-line wrapper over the P/Invoke, and it earns its place twice. Pester cannot
  mock a static .NET method, so every decision that branches on visibility -- notably
  Switch-GreenroomSession choosing which way to toggle -- would be untestable without
  a function to intercept. It is also the single place three callers read visibility
  from, rather than three separate casts of the same call.
#>

function Test-WindowVisible {
    [CmdletBinding()]
    [OutputType([bool])]
    param([Parameter(Mandatory)][IntPtr]$Handle)

    return [Greenroom.Win1]::IsWindowVisible($Handle)
}