Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

pi/agent/skillz/win32-drive/scripts/win32-drive.ps1

Raw
$raw = [Environment]::GetCommandLineArgs()
$scriptIndex = -1
$scriptName = [IO.Path]::GetFileName($PSCommandPath)
for ($i = 0; $i -lt $raw.Count; $i++) {
  try {
    if ($scriptIndex -lt 0 -and [IO.Path]::GetFileName([string]$raw[$i]) -eq $scriptName) { $scriptIndex = $i }
  } catch {}
}
$argv = @()
if ($scriptIndex -ge 0) {
  for ($i = $scriptIndex + 1; $i -lt $raw.Count; $i++) { $argv += [string] $raw[$i] }
}
$Command = 'help'
$Rest = @()
if ($argv.Count -gt 0) {
  $Command = [string] $argv[0]
  if ($argv.Count -gt 1) { $Rest = @($argv[1..($argv.Count - 1)]) }
}

$ErrorActionPreference = 'Stop'
$script:win32DriveStart = [Diagnostics.Stopwatch]::StartNew()

# Assemblies are loaded on demand: list/focus/window/uia never pay for WinForms.
function Import-Drawing {
  if (-not $script:win32DriveDrawing) { Add-Type -AssemblyName System.Drawing; $script:win32DriveDrawing = $true }
}

function Import-Forms {
  if (-not $script:win32DriveForms) { Import-Drawing; Add-Type -AssemblyName System.Windows.Forms; $script:win32DriveForms = $true }
}

$script:win32DriveNativeSource = @"
using System;
using System.Text;
using System.Runtime.InteropServices;

public static class Win32DriveNative {
  public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

  [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
  [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
  [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }

  [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
  [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
  [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassName(IntPtr hWnd, StringBuilder text, int count);
  [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
  [DllImport("dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr hwnd, int attr, out RECT rect, int size);
  [DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr hwnd, IntPtr hdcBlt, uint flags);
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
  [DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int w, int h, bool repaint);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int cmd);
  [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
  [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
  [DllImport("user32.dll")] public static extern void mouse_event(uint flags, uint dx, uint dy, uint data, UIntPtr extraInfo);
  [DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT point);
  [DllImport("user32.dll")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO info);
  [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint msg, UIntPtr wParam, string lParam, uint flags, uint timeout, out UIntPtr result);
  [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true, EntryPoint="SendMessageTimeoutW")] public static extern IntPtr SendMessageTimeoutPtr(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam, uint flags, uint timeout, out UIntPtr result);
  [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true, EntryPoint="SendMessageTimeoutW")] public static extern IntPtr SendMessageTimeoutText(IntPtr hWnd, uint msg, UIntPtr wParam, StringBuilder lParam, uint flags, uint timeout, out UIntPtr result);
  [DllImport("kernel32.dll")] public static extern uint GetTickCount();
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr value);
  [DllImport("shcore.dll")] public static extern int SetProcessDpiAwareness(int value);
  [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
  [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
  [DllImport("oleacc.dll")] public static extern int AccessibleObjectFromWindow(IntPtr hWnd, uint objectId, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object accessible);
  [DllImport("user32.dll")] public static extern bool IsHungAppWindow(IntPtr hWnd);
  [DllImport("user32.dll", EntryPoint="SystemParametersInfoW")] static extern bool SystemParametersInfoGet(uint action, uint param, ref uint value, uint winIni);
  [DllImport("user32.dll", EntryPoint="SystemParametersInfoW")] static extern bool SystemParametersInfoSet(uint action, uint param, IntPtr value, uint winIni);

  // Activation is asynchronous across processes, so every attempt has to be
  // confirmed by polling instead of by an immediate GetForegroundWindow.
  public static bool WaitForeground(IntPtr hWnd, int waitMs) {
    for (int waited = 0; ; waited += 40) {
      if (GetForegroundWindow() == hWnd) { return true; }
      if (waited >= waitMs) { return false; }
      System.Threading.Thread.Sleep(40);
    }
  }

  public static bool Foreground(IntPtr hWnd, int waitMs) {
    if (GetForegroundWindow() == hWnd) { return true; }
    BringWindowToTop(hWnd);
    SetForegroundWindow(hWnd);
    return WaitForeground(hWnd, waitMs);
  }

  // Windows refuses SetForegroundWindow from a background process while the
  // foreground lock timeout is running. Relaxing that timeout around a single
  // attempt is the safe workaround: AttachThreadInput would couple our input
  // queue to the foreground application and can hang it. The parameter is
  // restored only after activation settled, and without SPIF_SENDCHANGE so no
  // WM_SETTINGCHANGE storm hits every window on the desktop.
  public static uint GetForegroundLockTimeout() {
    uint value = 0;
    SystemParametersInfoGet(0x2000, 0, ref value, 0);
    return value;
  }

  public static bool ForegroundUnlocked(IntPtr hWnd, int waitMs) {
    const uint SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000;
    const uint SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001;
    uint previous = 0;
    if (!SystemParametersInfoGet(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, ref previous, 0) || previous == 0) { return false; }
    SystemParametersInfoSet(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, IntPtr.Zero, 0);
    try {
      return Foreground(hWnd, waitMs);
    } finally {
      SystemParametersInfoSet(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, new IntPtr(previous), 0);
    }
  }

  [DllImport("user32.dll", EntryPoint="SetWindowLongPtr")] static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int index, IntPtr value);
  [DllImport("user32.dll", EntryPoint="GetWindowLongPtr")] static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int index);
  [DllImport("user32.dll", EntryPoint="SetWindowLong")] static extern int SetWindowLong32(IntPtr hWnd, int index, int value);
  [DllImport("user32.dll", EntryPoint="GetWindowLong")] static extern int GetWindowLong32(IntPtr hWnd, int index);

  // Keeps the guard overlay out of the activation chain: without this the
  // overlay steals foreground from the window the command is about to drive.
  public static void AddExStyle(IntPtr hWnd, int bits) {
    const int GWL_EXSTYLE = -20;
    if (IntPtr.Size == 8) {
      long current = GetWindowLongPtr64(hWnd, GWL_EXSTYLE).ToInt64();
      SetWindowLongPtr64(hWnd, GWL_EXSTYLE, new IntPtr(current | (long) (uint) bits));
    } else {
      int current = GetWindowLong32(hWnd, GWL_EXSTYLE);
      SetWindowLong32(hWnd, GWL_EXSTYLE, current | bits);
    }
  }

  [StructLayout(LayoutKind.Sequential)] public struct KEYBDINPUT64 { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public ulong dwExtraInfo; }
  [StructLayout(LayoutKind.Sequential, Size=40)] public struct INPUT64 { public uint type; public uint pad; public KEYBDINPUT64 ki; }
  [StructLayout(LayoutKind.Sequential)] public struct KEYBDINPUT32 { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public uint dwExtraInfo; }
  [StructLayout(LayoutKind.Sequential, Size=28)] public struct INPUT32 { public uint type; public KEYBDINPUT32 ki; }

  [DllImport("user32.dll", SetLastError=true)] static extern uint SendInput(uint count, INPUT64[] inputs, int size);
  [DllImport("user32.dll", SetLastError=true, EntryPoint="SendInput")] static extern uint SendInput32(uint count, INPUT32[] inputs, int size);

  public static int TypeUnicode(string text) {
    const uint KEYEVENTF_KEYUP = 0x0002;
    const uint KEYEVENTF_UNICODE = 0x0004;
    const ushort VK_RETURN = 0x0D;
    int sent = 0;
    foreach (char ch in text) {
      if (ch == '\r') { continue; }
      bool enter = ch == '\n';
      ushort vk = enter ? VK_RETURN : (ushort) 0;
      ushort scan = enter ? (ushort) 0 : (ushort) ch;
      uint flags = enter ? 0u : KEYEVENTF_UNICODE;
      if (IntPtr.Size == 8) {
        INPUT64[] events = new INPUT64[2];
        events[0].type = 1; events[0].ki.wVk = vk; events[0].ki.wScan = scan; events[0].ki.dwFlags = flags;
        events[1] = events[0]; events[1].ki.dwFlags = flags | KEYEVENTF_KEYUP;
        sent += (int) SendInput(2, events, Marshal.SizeOf(typeof(INPUT64)));
      } else {
        INPUT32[] events = new INPUT32[2];
        events[0].type = 1; events[0].ki.wVk = vk; events[0].ki.wScan = scan; events[0].ki.dwFlags = flags;
        events[1] = events[0]; events[1].ki.dwFlags = flags | KEYEVENTF_KEYUP;
        sent += (int) SendInput32(2, events, Marshal.SizeOf(typeof(INPUT32)));
      }
    }
    return sent;
  }
}
"@

# Compiling this P/Invoke surface costs about a second and spawns csc.exe, which
# flashes a console window. Cache the assembly per source hash and reuse it.
function Import-Native([string] $Source) {
  if ('Win32DriveNative' -as [type]) { return 'loaded' }
  $sha = [Security.Cryptography.SHA256]::Create()
  try {
    $hash = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Source))) -replace '-', '').Substring(0, 16)
  } finally { $sha.Dispose() }
  $dir = Join-Path $env:LOCALAPPDATA 'win32-drive'
  $dll = Join-Path $dir "Win32DriveNative-$hash.dll"
  if (Test-Path $dll) {
    try {
      Add-Type -Path $dll
      return 'cache'
    } catch {}
  }
  if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
  $staging = Join-Path $dir ('build-' + [guid]::NewGuid().ToString('N') + '.dll')
  Add-Type -TypeDefinition $Source -OutputAssembly $staging -OutputType Library
  try { Move-Item -LiteralPath $staging -Destination $dll -Force } catch { Remove-Item -LiteralPath $staging -Force -ErrorAction SilentlyContinue }
  if (Test-Path $dll) { Add-Type -Path $dll } else { Add-Type -TypeDefinition $Source }
  'compiled'
}

$script:win32DriveNativeLoad = Import-Native $script:win32DriveNativeSource

function Set-DpiAwareness {
  try { if ([Win32DriveNative]::SetProcessDpiAwarenessContext([IntPtr] (-4))) { return 'per-monitor-v2' } } catch {}
  try { if ([Win32DriveNative]::SetProcessDpiAwareness(2) -eq 0) { return 'per-monitor' } } catch {}
  try { [void] [Win32DriveNative]::SetProcessDPIAware(); return 'system' } catch {}
  'unaware'
}

$script:win32DriveDpi = Set-DpiAwareness

function Write-Json($Value) {
  $Value | ConvertTo-Json -Depth 24 -Compress
}

# Errors carry a stable code, a hint, and whatever context saves the agent a
# follow-up query (candidate windows, foreground state, idle time).
function Fail([string] $Code, [string] $Message, [string] $Hint, $Extra) {
  $payload = [ordered] @{ ok = $false; error = $Message; code = $Code }
  if ($Hint) { $payload['hint'] = $Hint }
  if ($Extra) { foreach ($key in $Extra.Keys) { $payload[$key] = $Extra[$key] } }
  $script:win32DriveFailure = $payload
  throw $Message
}

function Get-FailurePayload([string] $Name, $ErrorRecord) {
  $message = $ErrorRecord.Exception.Message
  $payload = if ($script:win32DriveFailure -and $script:win32DriveFailure['error'] -eq $message) {
    $script:win32DriveFailure
  } else {
    [ordered] @{ ok = $false; error = $message; code = 'unexpected' }
  }
  $script:win32DriveFailure = $null
  $payload['cmd'] = $Name
  [pscustomobject] $payload
}

$script:win32DriveValueOptions = @('Hwnd','Process','TitleContains','X','Y','Width','Height','Out','Text','Keys','Action','Button','Count','DurationMs','IdleMs','TimeoutMs','CountdownMs','Set','Depth','Mode','Scope','Focus','Cursor','Return','Refresh','Method','Name','AutomationId','ClassName','ControlType','Index','Value','Json','File')
$script:win32DriveFlagOptions = @('Pointer','ContinueOnError','NoCountdown')

function Parse-Options([string[]] $ArgList) {
  $opts = @{}
  $pos = @()
  for ($i = 0; $i -lt $ArgList.Count; $i++) {
    $arg = $ArgList[$i]
    if ($arg.StartsWith('-')) {
      $key = $arg.TrimStart('-')
      if ($script:win32DriveFlagOptions -contains $key) { $opts[$key] = $true; continue }
      if ($script:win32DriveValueOptions -notcontains $key) {
        Fail 'unknown-option' "Unknown option: -$key" 'run help for the option catalog' @{ knownOptions = @($script:win32DriveValueOptions + $script:win32DriveFlagOptions) }
      }
      if ($i + 1 -ge $ArgList.Count) { Fail 'missing-value' "Missing value for -$key" "pass a value after -$key" }
      $i++
      $opts[$key] = $ArgList[$i]
    } else {
      $pos += $arg
    }
  }
  if ($pos.Count -gt 0) { Fail 'positional-args' ('Unexpected positional args: ' + ($pos -join ' ')) 'every input is a named option, for example -Text hello' }
  $opts
}

function Int-Opt($Opts, [string] $Name, $Default = $null) {
  if (-not $Opts.ContainsKey($Name)) { return $Default }
  [int] $Opts[$Name]
}

function Long-Opt($Opts, [string] $Name, $Default = $null) {
  if (-not $Opts.ContainsKey($Name)) { return $Default }
  [int64] $Opts[$Name]
}

function Str-Opt($Opts, [string] $Name, $Default = $null) {
  if (-not $Opts.ContainsKey($Name)) { return $Default }
  [string] $Opts[$Name]
}

function Get-ProcessNames {
  if ($null -ne $script:win32DriveProcessNames) { return $script:win32DriveProcessNames }
  $map = @{}
  foreach ($proc in (Get-Process -ErrorAction SilentlyContinue)) { $map[[int64] $proc.Id] = $proc.ProcessName }
  $script:win32DriveProcessNames = $map
  $map
}

function Get-Windows {
  $script:win32DriveWindows = @()
  $script:win32DriveForeground = [Win32DriveNative]::GetForegroundWindow()
  $names = Get-ProcessNames
  [Win32DriveNative]::EnumWindows({ param($h, $l)
    if ([Win32DriveNative]::IsWindowVisible($h)) {
      $sb = [Text.StringBuilder]::new(1024)
      [void] [Win32DriveNative]::GetWindowText($h, $sb, $sb.Capacity)
      $title = $sb.ToString()
      if ($title.Trim().Length -gt 0) {
        [uint32] $procId = 0
        [void] [Win32DriveNative]::GetWindowThreadProcessId($h, [ref] $procId)
        $rect = Get-Rect $h
        $script:win32DriveWindows += [pscustomobject] [ordered] @{
          hwnd = $h.ToInt64()
          pid = [int64] $procId
          process = $names[[int64] $procId]
          title = $title
          x = $rect.Left
          y = $rect.Top
          width = $rect.Right - $rect.Left
          height = $rect.Bottom - $rect.Top
          z = $script:win32DriveWindows.Count
          minimized = [bool] [Win32DriveNative]::IsIconic($h)
          focused = $h -eq $script:win32DriveForeground
        }
      }
    }
    $true
  }, [IntPtr]::Zero) | Out-Null
  $script:win32DriveWindows
}

function Get-Rect([IntPtr] $Hwnd) {
  $rect = New-Object Win32DriveNative+RECT
  $size = [Runtime.InteropServices.Marshal]::SizeOf([type] [Win32DriveNative+RECT])
  $hr = [Win32DriveNative]::DwmGetWindowAttribute($Hwnd, 9, [ref] $rect, $size)
  if ($hr -ne 0) { [void] [Win32DriveNative]::GetWindowRect($Hwnd, [ref] $rect) }
  $rect
}

function Get-RawRect([IntPtr] $Hwnd) {
  $rect = New-Object Win32DriveNative+RECT
  [void] [Win32DriveNative]::GetWindowRect($Hwnd, [ref] $rect)
  $rect
}

function Has-Selector($Opts) {
  $Opts.ContainsKey('Hwnd') -or $Opts.ContainsKey('Process') -or $Opts.ContainsKey('TitleContains')
}

function Get-FocusMode($Opts) {
  $focus = (Str-Opt $Opts 'Focus' 'require').ToLowerInvariant()
  if (@('auto','require','skip') -notcontains $focus) { Fail 'invalid-value' 'Focus must be auto, require, or skip' 'require uses activation APIs only, auto adds a caption click, skip does not focus' }
  if ($focus -eq 'auto' -and -not $Opts.ContainsKey('Pointer')) {
    Fail 'pointer-consent' 'Focus auto falls back to clicking the target caption' 'ask the user first, then pass -Pointer'
  }
  $focus
}

function Get-ForegroundInfo {
  $h = [Win32DriveNative]::GetForegroundWindow()
  $title = ''
  [uint32] $procId = 0
  if ($h -ne [IntPtr]::Zero) {
    $sb = [Text.StringBuilder]::new(1024)
    [void] [Win32DriveNative]::GetWindowText($h, $sb, $sb.Capacity)
    $title = $sb.ToString()
    [void] [Win32DriveNative]::GetWindowThreadProcessId($h, [ref] $procId)
  }
  $proc = (Get-ProcessNames)[[int64] $procId]
  $hung = $false
  try { $hung = [bool] [Win32DriveNative]::IsHungAppWindow($h) } catch {}
  [pscustomobject] [ordered] @{
    hwnd = $h.ToInt64()
    pid = [int64] $procId
    process = $proc
    title = $title
    hung = $hung
  }
}

function Invoke-PointerClick([int] $X, [int] $Y, [string] $Button = 'left', [int] $Count = 1, [bool] $RestoreCursor = $true) {
  $origin = New-Object Win32DriveNative+POINT
  [void] [Win32DriveNative]::GetCursorPos([ref] $origin)
  [void] [Win32DriveNative]::SetCursorPos($X, $Y)
  $down = if ($Button -eq 'right') { 0x0008 } else { 0x0002 }
  $up = if ($Button -eq 'right') { 0x0010 } else { 0x0004 }
  for ($i = 0; $i -lt $Count; $i++) {
    [Win32DriveNative]::mouse_event($down, 0, 0, 0, [UIntPtr]::Zero)
    Start-Sleep -Milliseconds 45
    [Win32DriveNative]::mouse_event($up, 0, 0, 0, [UIntPtr]::Zero)
    Start-Sleep -Milliseconds 80
  }
  if ($RestoreCursor) { [void] [Win32DriveNative]::SetCursorPos($origin.X, $origin.Y) }
}

# Activation APIs are denied to background processes on locked-down foreground
# settings, but real pointer input always activates the window it lands on.
function Invoke-CaptionClick([IntPtr] $Hwnd) {
  $r = Get-Rect $Hwnd
  $width = $r.Right - $r.Left
  if ($width -le 0) { return $false }
  Invoke-PointerClick ([int] ($r.Left + $width * 0.35)) ([int] ($r.Top + 6)) 'left' 1 $true
  [Win32DriveNative]::WaitForeground($Hwnd, 800)
}

function Set-Foreground([IntPtr] $Hwnd, [string] $Mode = 'auto', [int] $TimeoutMs = 2500) {
  if ([Win32DriveNative]::IsIconic($Hwnd)) {
    Fail 'window-minimized' 'Target window is minimized' ('restore it first: window -Hwnd ' + $Hwnd.ToInt64() + ' -Action restore') @{ hwnd = $Hwnd.ToInt64() }
  }
  $deadline = (Get-Date).AddMilliseconds($TimeoutMs)
  do {
    # Windows denies activation to background processes while the user is
    # actually typing or moving the mouse, so only try during input gaps.
    if ((Get-IdleMs) -ge 250) {
      if ([Win32DriveNative]::Foreground($Hwnd, 300)) { return }
      if ([Win32DriveNative]::ForegroundUnlocked($Hwnd, 400)) { return }
    }
    Start-Sleep -Milliseconds 80
  } while ((Get-Date) -lt $deadline)
  if ($Mode -eq 'auto' -and (Invoke-CaptionClick $Hwnd)) { return }
  $fg = Get-ForegroundInfo
  $reason = if ($fg.hung) { 'the current foreground window is not responding' } elseif ((Get-IdleMs) -lt 500) { 'the user is still providing input' } else { 'foreground lock' }
  $hint = if ($fg.hung) {
    'nothing can be driven until that window recovers'
  } elseif ($Mode -eq 'require') {
    'ask the user, then retry with -Focus auto -Pointer, which clicks the target caption'
  } else {
    'retry when the user is idle, or drive the app with uia-invoke/uia-set instead'
  }
  Fail 'focus-denied' ('Could not focus target window ' + $Hwnd.ToInt64() + ': ' + $reason) $hint @{
    hwnd = $Hwnd.ToInt64()
    foreground = $fg
    lockTimeoutMs = [int64] [Win32DriveNative]::GetForegroundLockTimeout()
    idleMs = Get-IdleMs
    focusMode = $Mode
  }
}

function Select-Windows($Opts) {
  $wins = @(Get-Windows)
  $proc = Str-Opt $Opts 'Process'
  $title = Str-Opt $Opts 'TitleContains'
  if ($Opts.ContainsKey('Hwnd')) {
    $wanted = Long-Opt $Opts 'Hwnd'
    $wins = @($wins | Where-Object { $_.hwnd -eq $wanted })
  }
  if ($proc) { $wins = @($wins | Where-Object { $_.process -eq $proc }) }
  if ($title) { $wins = @($wins | Where-Object { $_.title.IndexOf($title, [StringComparison]::OrdinalIgnoreCase) -ge 0 }) }
  $wins
}

function Get-WindowInfo([IntPtr] $Hwnd) {
  @(Get-Windows) | Where-Object { $_.hwnd -eq $Hwnd.ToInt64() } | Select-Object -First 1
}

function Resolve-Window($Opts) {
  if ($Opts.ContainsKey('Hwnd')) {
    $hwnd = [IntPtr] (Long-Opt $Opts 'Hwnd')
    if (-not [Win32DriveNative]::IsWindow($hwnd)) {
      Fail 'stale-window' 'Invalid window handle' 'the window closed or the handle is wrong: run list again' @{ hwnd = $hwnd.ToInt64() }
    }
    return $hwnd
  }
  $wins = @(Select-Windows $Opts)
  if ($wins.Count -eq 0) {
    $proc = Str-Opt $Opts 'Process'
    $candidates = if ($proc) {
      @(Get-Windows | Where-Object { $_.process -like "*$proc*" } | Select-Object -First 8)
    } else {
      @(Get-Windows | Select-Object -First 8)
    }
    Fail 'no-window' 'No matching window' 'relax the selector; candidates lists what is open right now' @{ candidates = $candidates }
  }
  if ($wins.Count -gt 1) {
    Fail 'ambiguous-window' ('Ambiguous window: ' + $wins.Count + ' matches') 'pick one -Hwnd from candidates' @{ candidates = @($wins | Select-Object -First 8) }
  }
  [IntPtr] $wins[0].hwnd
}

function Get-IdleMs {
  $info = New-Object Win32DriveNative+LASTINPUTINFO
  $info.cbSize = [Runtime.InteropServices.Marshal]::SizeOf($info)
  [void] [Win32DriveNative]::GetLastInputInfo([ref] $info)
  [int64] ([Win32DriveNative]::GetTickCount() - $info.dwTime)
}

function Wait-UserIdle([int] $IdleMs, [int] $TimeoutMs) {
  $start = Get-Date
  while ((Get-IdleMs) -lt $IdleMs) {
    if (((Get-Date) - $start).TotalMilliseconds -gt $TimeoutMs) {
      Fail 'user-active' "User not idle for $IdleMs ms" 'the human is using the machine: retry later, raise -TimeoutMs, or use a background command' @{ idleMs = Get-IdleMs; requiredIdleMs = $IdleMs }
    }
    Start-Sleep -Milliseconds 100
  }
}

# One overlay renderer: quick flash for background commands, banner plus
# countdown for foreground takeovers, which the human can veto by touching
# keyboard or mouse.
function Show-Overlay($X = $null, $Y = $null, [string] $Banner = $null, [int] $DurationMs = 180, [bool] $AbortOnInput = $false) {
  Import-Forms
  $state = [pscustomobject] @{ Banner = $Banner; Remaining = $DurationMs }
  $bannerScreen = if ($null -ne $X -and $null -ne $Y) {
    [Windows.Forms.Screen]::FromPoint([Drawing.Point]::new([int] $X, [int] $Y))
  } else {
    [Windows.Forms.Screen]::PrimaryScreen
  }
  $forms = @()
  foreach ($screen in [Windows.Forms.Screen]::AllScreens) {
    $b = $screen.Bounds
    $targetX = $X
    $targetY = $Y
    $showBanner = $Banner -and $screen.DeviceName -eq $bannerScreen.DeviceName
    $form = New-Object Windows.Forms.Form
    $form.FormBorderStyle = 'None'
    $form.StartPosition = 'Manual'
    $form.Bounds = $b
    $form.TopMost = $true
    $form.BackColor = [Drawing.Color]::Magenta
    $form.TransparencyKey = [Drawing.Color]::Magenta
    $form.ShowInTaskbar = $false
    $form.Opacity = 0.0
    $form.Add_Paint({
      param($sender, $event)
      $event.Graphics.SmoothingMode = [Drawing.Drawing2D.SmoothingMode]::AntiAlias
      $pen = New-Object Drawing.Pen ([Drawing.Color]::Red), 16
      try {
        $event.Graphics.DrawRectangle($pen, 8, 8, $sender.Width - 17, $sender.Height - 17)
        if ($targetX -ne $null -and $targetY -ne $null -and $b.Contains([Drawing.Point]::new([int]$targetX, [int]$targetY))) {
          $localX = [int]$targetX - $b.Left
          $localY = [int]$targetY - $b.Top
          $event.Graphics.DrawEllipse($pen, $localX - 24, $localY - 24, 48, 48)
        }
        if ($showBanner) {
          $text = $state.Banner
          if ($state.Remaining -gt 0) {
            $text = $text + "`nacting in " + [Math]::Max(0, [Math]::Round($state.Remaining / 1000.0, 1)) + 's - touch keyboard or mouse to cancel'
          }
          $font = New-Object Drawing.Font 'Segoe UI', 20, ([Drawing.FontStyle]::Bold)
          $back = New-Object Drawing.SolidBrush ([Drawing.Color]::FromArgb(235, 20, 0, 0))
          $fore = New-Object Drawing.SolidBrush ([Drawing.Color]::White)
          try {
            $size = $event.Graphics.MeasureString($text, $font)
            $boxWidth = $size.Width + 48
            $boxHeight = $size.Height + 28
            $boxX = ($sender.Width - $boxWidth) / 2
            $boxY = 48
            $event.Graphics.FillRectangle($back, $boxX, $boxY, $boxWidth, $boxHeight)
            $event.Graphics.DrawRectangle($pen, $boxX, $boxY, $boxWidth, $boxHeight)
            $event.Graphics.DrawString($text, $font, $fore, $boxX + 24, $boxY + 14)
          } finally {
            $font.Dispose(); $back.Dispose(); $fore.Dispose()
          }
        }
      } finally {
        $pen.Dispose()
      }
    }.GetNewClosure())
    # WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW: never take focus,
    # never swallow input, never appear in the alt-tab list.
    [Win32DriveNative]::AddExStyle($form.Handle, (0x08000000 -bor 0x00000020 -bor 0x00000080))
    [void] [Win32DriveNative]::ShowWindow($form.Handle, 4)
    $forms += $form
  }
  foreach ($opacity in 0.2,0.45,0.7,0.9) {
    foreach ($form in $forms) { $form.Opacity = $opacity; $form.Invalidate() }
    [Windows.Forms.Application]::DoEvents()
    Start-Sleep -Milliseconds 20
  }
  $completed = $true
  if ($DurationMs -gt 0) {
    $deadline = (Get-Date).AddMilliseconds($DurationMs)
    while ($true) {
      $remaining = ($deadline - (Get-Date)).TotalMilliseconds
      if ($remaining -le 0) { break }
      $state.Remaining = [int] $remaining
      if ($Banner) { foreach ($form in $forms) { $form.Invalidate() } }
      [Windows.Forms.Application]::DoEvents()
      Start-Sleep -Milliseconds 50
      if ($AbortOnInput -and (Get-IdleMs) -lt 150) { $completed = $false; break }
    }
  }
  foreach ($opacity in 0.7,0.45,0.2,0.0) {
    foreach ($form in $forms) { $form.Opacity = $opacity }
    [Windows.Forms.Application]::DoEvents()
    Start-Sleep -Milliseconds 20
  }
  foreach ($form in $forms) { $form.Close(); $form.Dispose() }
  [Windows.Forms.Application]::DoEvents()
  $completed
}

function Draw-Indicator($X = $null, $Y = $null, [int] $DurationMs = 180) {
  [void] (Show-Overlay $X $Y $null $DurationMs $false)
}

# Background commands: wait for an input gap, flash once, act.
function Invoke-RacingGuard($Opts, $X = $null, $Y = $null) {
  $idle = Int-Opt $Opts 'IdleMs' 800
  $timeout = Int-Opt $Opts 'TimeoutMs' 10000
  Wait-UserIdle $idle $timeout
  Draw-Indicator $X $Y 120
}

# Foreground/pointer commands: same wait, then a named banner with a countdown
# the human can veto.
function Invoke-Takeover($Opts, [string] $Label, $X = $null, $Y = $null) {
  $idle = Int-Opt $Opts 'IdleMs' 800
  $timeout = Int-Opt $Opts 'TimeoutMs' 10000
  Wait-UserIdle $idle $timeout
  $countdown = if ($Opts.ContainsKey('NoCountdown')) { 0 } else { Int-Opt $Opts 'CountdownMs' 1200 }
  if (-not (Show-Overlay $X $Y $Label $countdown $true)) {
    Fail 'takeover-aborted' 'Foreground takeover cancelled by user input' 'the human took the machine back mid-countdown: ask before retrying' @{ label = $Label }
  }
}

function Theme-Name($Value) {
  if ([int]$Value -eq 0) { 'dark' } else { 'light' }
}

function Theme-Value([string] $Mode) {
  switch ($Mode.ToLowerInvariant()) {
    'dark' { 0 }
    'light' { 1 }
    default { throw 'Mode must be dark or light' }
  }
}

function Get-Theme {
  $path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
  $item = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
  $apps = if ($item -and $null -ne $item.AppsUseLightTheme) { $item.AppsUseLightTheme } else { 1 }
  $system = if ($item -and $null -ne $item.SystemUsesLightTheme) { $item.SystemUsesLightTheme } else { 1 }
  [pscustomobject] [ordered] @{ ok = $true; apps = Theme-Name $apps; system = Theme-Name $system }
}

function Broadcast-ThemeChange([string] $Refresh) {
  $result = [UIntPtr]::Zero
  foreach ($name in @('ImmersiveColorSet','WindowsThemeElement','Software\Microsoft\Windows\CurrentVersion\Themes\Personalize')) {
    [void] [Win32DriveNative]::SendMessageTimeout([IntPtr]0xffff, 0x001A, [UIntPtr]::Zero, $name, 2, 5000, [ref] $result)
  }
  [void] [Win32DriveNative]::SendMessageTimeout([IntPtr]0xffff, 0x031A, [UIntPtr]::Zero, $null, 2, 5000, [ref] $result)
  if ($Refresh -eq 'full') {
    Start-Process -WindowStyle Hidden -FilePath rundll32.exe -ArgumentList 'user32.dll,UpdatePerUserSystemParameters 1, True' -Wait
  }
}

function Set-Theme([string] $Mode, [string] $Scope, [string] $Refresh) {
  $value = Theme-Value $Mode
  $scopeName = if ($Scope) { $Scope.ToLowerInvariant() } else { 'both' }
  if (@('apps','system','both') -notcontains $scopeName) { throw 'Scope must be apps, system, or both' }
  $refreshName = if ($Refresh) { $Refresh.ToLowerInvariant() } else { 'broadcast' }
  if (@('broadcast','full') -notcontains $refreshName) { throw 'Refresh must be broadcast or full' }
  $previous = Get-Theme
  $path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
  New-Item -Path $path -Force | Out-Null
  if ($scopeName -eq 'apps' -or $scopeName -eq 'both') { Set-ItemProperty -Path $path -Name AppsUseLightTheme -Type DWord -Value $value }
  if ($scopeName -eq 'system' -or $scopeName -eq 'both') { Set-ItemProperty -Path $path -Name SystemUsesLightTheme -Type DWord -Value $value }
  Broadcast-ThemeChange $refreshName
  $current = Get-Theme
  [pscustomobject] [ordered] @{
    ok = $true
    apps = $current.apps
    system = $current.system
    scope = $scopeName
    refresh = $refreshName
    previous = [ordered] @{ apps = $previous.apps; system = $previous.system }
  }
}

function Save-Bitmap($Bitmap, [string] $Out) {
  if (-not $Out) { $Out = Join-Path $env:TEMP ("win32-drive-shot-" + [guid]::NewGuid().ToString('N') + '.png') }
  $full = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Out)
  $parent = Split-Path -Parent $full
  if ($parent -and -not (Test-Path $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null }
  $Bitmap.Save($full, [Drawing.Imaging.ImageFormat]::Png)
  $full
}

function Get-PixelColor([int] $X, [int] $Y) {
  Import-Drawing
  $bmp = [Drawing.Bitmap]::new(1, 1)
  $g = [Drawing.Graphics]::FromImage($bmp)
  try {
    $g.CopyFromScreen($X, $Y, 0, 0, [Drawing.Size]::new(1, 1))
    $c = $bmp.GetPixel(0, 0)
    [pscustomobject] [ordered] @{ ok = $true; x = $X; y = $Y; r = $c.R; g = $c.G; b = $c.B; hex = ('#{0:X2}{1:X2}{2:X2}' -f $c.R, $c.G, $c.B) }
  } finally {
    $g.Dispose(); $bmp.Dispose()
  }
}

function Capture-Region([int] $X, [int] $Y, [int] $Width, [int] $Height, [string] $Out) {
  Import-Drawing
  $bmp = [Drawing.Bitmap]::new($Width, $Height)
  $g = [Drawing.Graphics]::FromImage($bmp)
  try {
    $g.CopyFromScreen($X, $Y, 0, 0, $bmp.Size)
    $path = Save-Bitmap $bmp $Out
    [pscustomobject] [ordered] @{ ok = $true; path = $path; x = $X; y = $Y; width = $Width; height = $Height; mode = 'region' }
  } finally {
    $g.Dispose(); $bmp.Dispose()
  }
}

function Test-BlankBitmap($Bitmap) {
  $first = $null
  for ($ix = 0; $ix -lt 5; $ix++) {
    for ($iy = 0; $iy -lt 5; $iy++) {
      $px = [int] (($Bitmap.Width - 1) * $ix / 4)
      $py = [int] (($Bitmap.Height - 1) * $iy / 4)
      $argb = $Bitmap.GetPixel($px, $py).ToArgb()
      if ($null -eq $first) { $first = $argb } elseif ($argb -ne $first) { return $false }
    }
  }
  $true
}

function Capture-Window([IntPtr] $Hwnd, [string] $Out) {
  Import-Drawing
  $frame = Get-Rect $Hwnd
  $raw = Get-RawRect $Hwnd
  $w = $frame.Right - $frame.Left
  $h = $frame.Bottom - $frame.Top
  $rawW = $raw.Right - $raw.Left
  $rawH = $raw.Bottom - $raw.Top
  if ($w -le 0 -or $h -le 0 -or $rawW -le 0 -or $rawH -le 0) { throw 'Window has empty bounds' }
  $used = 'printwindow'
  $image = $null
  $bmp = [Drawing.Bitmap]::new($rawW, $rawH)
  $g = [Drawing.Graphics]::FromImage($bmp)
  try {
    $hdc = $g.GetHdc()
    $ok = [Win32DriveNative]::PrintWindow($Hwnd, $hdc, 2)
    $g.ReleaseHdc($hdc)
    if ($ok -and (Test-BlankBitmap $bmp)) { $ok = $false }
    if ($ok) {
      # PrintWindow renders from the raw window rect; crop back to the DWM frame
      # bounds so capture pixels share the coordinate space of list/click/uia.
      $cropX = [Math]::Max(0, $frame.Left - $raw.Left)
      $cropY = [Math]::Max(0, $frame.Top - $raw.Top)
      $cropW = [Math]::Min($w, $rawW - $cropX)
      $cropH = [Math]::Min($h, $rawH - $cropY)
      $image = $bmp.Clone([Drawing.Rectangle]::new($cropX, $cropY, $cropW, $cropH), $bmp.PixelFormat)
    } else {
      $used = 'screen-crop'
      $image = [Drawing.Bitmap]::new($w, $h)
      $shotGraphics = [Drawing.Graphics]::FromImage($image)
      try { $shotGraphics.CopyFromScreen($frame.Left, $frame.Top, 0, 0, $image.Size) } finally { $shotGraphics.Dispose() }
    }
    $path = Save-Bitmap $image $Out
    [pscustomobject] [ordered] @{ ok = $true; path = $path; hwnd = $Hwnd.ToInt64(); x = $frame.Left; y = $frame.Top; width = $image.Width; height = $image.Height; mode = $used }
  } finally {
    $g.Dispose(); $bmp.Dispose()
    if ($image) { $image.Dispose() }
  }
}

function Rect-Int($Value) {
  $d = [double] $Value
  if ([double]::IsNaN($d) -or [double]::IsInfinity($d)) { return $null }
  [int] [Math]::Round($d)
}

function Dump-Uia($Element, [int] $Depth, [int] $MaxDepth) {
  if ($null -eq $Element) { return $null }
  $item = [ordered] @{
    name = $null
    automationId = $null
    className = $null
    controlType = $null
    x = $null
    y = $null
    width = $null
    height = $null
    truncated = $false
    error = $null
    children = @()
  }
  try {
    $current = $Element.Current
    $rect = $current.BoundingRectangle
    $item.name = $current.Name
    $item.automationId = $current.AutomationId
    $item.className = $current.ClassName
    $item.controlType = $current.ControlType.ProgrammaticName
    $item.x = Rect-Int $rect.X
    $item.y = Rect-Int $rect.Y
    $item.width = Rect-Int $rect.Width
    $item.height = Rect-Int $rect.Height
  } catch {
    $item.error = $_.Exception.Message
    return [pscustomobject] $item
  }
  if ($Depth -lt $MaxDepth) {
    try {
      $children = $Element.FindAll([Windows.Automation.TreeScope]::Children, [Windows.Automation.Condition]::TrueCondition)
      foreach ($child in $children) { $item.children += Dump-Uia $child ($Depth + 1) $MaxDepth }
    } catch {
      $item.error = $_.Exception.Message
    }
  } else {
    try {
      if ($null -ne $Element.FindFirst([Windows.Automation.TreeScope]::Children, [Windows.Automation.Condition]::TrueCondition)) { $item.truncated = $true }
    } catch {}
  }
  [pscustomobject] $item
}

function Import-Uia {
  Add-Type -AssemblyName UIAutomationClient,UIAutomationTypes
}

function Get-UiaControlType([string] $Name) {
  $field = [Windows.Automation.ControlType].GetField($Name, 'Public,Static,IgnoreCase')
  if ($null -eq $field) { throw "Unknown control type: $Name" }
  $field.GetValue($null)
}

function Describe-UiaElement($Element) {
  $rect = $Element.Current.BoundingRectangle
  [pscustomobject] [ordered] @{
    name = $Element.Current.Name
    automationId = $Element.Current.AutomationId
    className = $Element.Current.ClassName
    controlType = $Element.Current.ControlType.ProgrammaticName
    x = Rect-Int $rect.X
    y = Rect-Int $rect.Y
    width = Rect-Int $rect.Width
    height = Rect-Int $rect.Height
  }
}

# Element addressing for the focus-free action commands: any combination of
# -Name, -AutomationId, -ClassName, -ControlType, disambiguated by -Index.
function Resolve-UiaElement($Opts) {
  Import-Uia
  $root = if (Has-Selector $Opts) {
    [Windows.Automation.AutomationElement]::FromHandle((Resolve-Window $Opts))
  } else {
    Fail 'missing-selector' 'element commands need a window selector' 'pass -Hwnd, -Process, or -TitleContains; list shows all windows'
  }
  if (-not ($Opts.ContainsKey('Name') -or $Opts.ContainsKey('AutomationId') -or $Opts.ContainsKey('ClassName') -or $Opts.ContainsKey('ControlType'))) {
    Fail 'missing-filter' 'element commands need -Name, -AutomationId, -ClassName, or -ControlType' 'read the tree with uia first, then address the element; filters accept wildcards'
  }
  # ControlType narrows the tree walk; the string filters are wildcards, because
  # bridged providers generate volatile class names and automation ids.
  $condition = if ($Opts.ContainsKey('ControlType')) {
    New-Object Windows.Automation.PropertyCondition ([Windows.Automation.AutomationElement]::ControlTypeProperty), (Get-UiaControlType (Str-Opt $Opts 'ControlType'))
  } else {
    [Windows.Automation.Condition]::TrueCondition
  }
  $found = @($root.FindAll([Windows.Automation.TreeScope]::Descendants, $condition))
  foreach ($filter in @(@('Name','Name'), @('AutomationId','AutomationId'), @('ClassName','ClassName'))) {
    if (-not $Opts.ContainsKey($filter[0])) { continue }
    $pattern = Str-Opt $Opts $filter[0]
    $property = $filter[1]
    $found = @($found | Where-Object { [string] $_.Current.$property -like $pattern })
  }
  if ($found.Count -eq 0) {
    $sample = @($root.FindAll([Windows.Automation.TreeScope]::Children, [Windows.Automation.Condition]::TrueCondition) | Select-Object -First 8 | ForEach-Object { Describe-UiaElement $_ })
    Fail 'no-element' 'No matching element' 'filters are wildcards against the uia tree; candidates lists the direct children' @{ candidates = $sample }
  }
  if ($Opts.ContainsKey('Index')) {
    $index = Int-Opt $Opts 'Index'
    if ($index -lt 0 -or $index -ge $found.Count) {
      Fail 'index-out-of-range' ('Index out of range: ' + $found.Count + ' matching elements') 'use an index between 0 and matches-1' @{ matches = $found.Count }
    }
    return $found[$index]
  }
  if ($found.Count -gt 1) {
    Fail 'ambiguous-element' ('Ambiguous element: ' + $found.Count + ' matches') 'narrow the filters or pass -Index' @{ candidates = @($found | Select-Object -First 8 | ForEach-Object { Describe-UiaElement $_ }) }
  }
  $found[0]
}

# Acts through accessibility patterns: no activation, no pointer, no keyboard,
# so the user's foreground window and cursor stay untouched.
function Invoke-UiaElement($Element) {
  $pattern = $null
  if ($Element.TryGetCurrentPattern([Windows.Automation.InvokePattern]::Pattern, [ref] $pattern)) { $pattern.Invoke(); return 'Invoke' }
  if ($Element.TryGetCurrentPattern([Windows.Automation.TogglePattern]::Pattern, [ref] $pattern)) { $pattern.Toggle(); return 'Toggle' }
  if ($Element.TryGetCurrentPattern([Windows.Automation.SelectionItemPattern]::Pattern, [ref] $pattern)) { $pattern.Select(); return 'SelectionItem' }
  if ($Element.TryGetCurrentPattern([Windows.Automation.ExpandCollapsePattern]::Pattern, [ref] $pattern)) {
    if ($pattern.Current.ExpandCollapseState -eq [Windows.Automation.ExpandCollapseState]::Expanded) { $pattern.Collapse(); return 'Collapse' }
    $pattern.Expand(); return 'Expand'
  }
  # Apps that only ship MSAA (WinForms, old Win32) surface as pattern-less panes
  # backed by a real HWND; both fallbacks below still avoid focus and pointer.
  $native = Get-UiaNativeHandle $Element
  if ($native -ne [IntPtr]::Zero) {
    if (Invoke-MsaaDefaultAction $native) { return 'MsaaDoDefaultAction' }
    if ((Get-WindowClassName $native) -match 'BUTTON') {
      [void] [Win32DriveNative]::PostMessage($native, 0x00F5, [IntPtr]::Zero, [IntPtr]::Zero)
      return 'BM_CLICK'
    }
  }
  Fail 'no-pattern' 'Element supports no invokable pattern' 'this app exposes no usable accessibility action: ask the user, then use click -Pointer' @{ element = (Describe-UiaElement $Element) }
}

function Set-UiaElementValue($Element, [string] $Value) {
  $pattern = $null
  if ($Element.TryGetCurrentPattern([Windows.Automation.ValuePattern]::Pattern, [ref] $pattern)) {
    if ($pattern.Current.IsReadOnly) { throw 'Element value is read-only' }
    $pattern.SetValue($Value)
    return 'Value'
  }
  $native = Get-UiaNativeHandle $Element
  if ($native -ne [IntPtr]::Zero -and (Send-WindowText $native $Value)) { return 'WM_CHAR' }
  Fail 'no-pattern' 'Element exposes no settable value' 'ask the user, then focus it and use type -Pointer' @{ element = (Describe-UiaElement $Element) }
}

# Posted characters reach a native edit control without focus and still raise
# the app's change notifications, which WM_SETTEXT alone suppresses.
function Send-WindowText([IntPtr] $Hwnd, [string] $Value) {
  $result = [UIntPtr]::Zero
  if ([Win32DriveNative]::SendMessageTimeoutPtr($Hwnd, 0x00B1, [UIntPtr]::Zero, [IntPtr] (-1), 2, 2000, [ref] $result) -eq [IntPtr]::Zero) { return $false }
  if ($Value.Length -eq 0) {
    [void] [Win32DriveNative]::PostMessage($Hwnd, 0x0102, [IntPtr] 8, [IntPtr] 1)
  }
  foreach ($ch in $Value.ToCharArray()) {
    if ($ch -eq "`r") { continue }
    $code = if ($ch -eq "`n") { 13 } else { [int] $ch }
    [void] [Win32DriveNative]::PostMessage($Hwnd, 0x0102, [IntPtr] $code, [IntPtr] 1)
  }
  # Posted messages are processed by the target's own loop, so confirm by polling.
  $expected = $Value.Replace("`r", '')
  $deadline = (Get-Date).AddMilliseconds(1200)
  do {
    Start-Sleep -Milliseconds 50
    if ((Get-RemoteWindowText $Hwnd).Replace("`r", '') -eq $expected) { return $true }
  } while ((Get-Date) -lt $deadline)
  $false
}

# GetWindowText does not cross process boundaries for control text; WM_GETTEXT does.
function Get-RemoteWindowText([IntPtr] $Hwnd) {
  $buffer = [Text.StringBuilder]::new(8192)
  $result = [UIntPtr]::Zero
  [void] [Win32DriveNative]::SendMessageTimeoutText($Hwnd, 0x000D, [UIntPtr]::new([uint32] $buffer.Capacity), $buffer, 2, 2000, [ref] $result)
  $buffer.ToString()
}

function Get-UiaNativeHandle($Element) {
  try { [IntPtr] $Element.Current.NativeWindowHandle } catch { [IntPtr]::Zero }
}

function Get-WindowClassName([IntPtr] $Hwnd) {
  $sb = [Text.StringBuilder]::new(256)
  [void] [Win32DriveNative]::GetClassName($Hwnd, $sb, $sb.Capacity)
  $sb.ToString()
}

function Invoke-MsaaDefaultAction([IntPtr] $Hwnd) {
  $iid = [guid] '618736e0-3c3d-11cf-810c-00aa00389b71'
  $accessible = $null
  try {
    if ([Win32DriveNative]::AccessibleObjectFromWindow($Hwnd, 0xFFFFFFFC, [ref] $iid, [ref] $accessible) -ne 0) { return $false }
    if ($null -eq $accessible) { return $false }
    [void] $accessible.GetType().InvokeMember('accDoDefaultAction', 'InvokeMethod', $null, $accessible, @([int] 0))
    $true
  } catch {
    $false
  }
}

function Escape-SendKeysText([string] $Text) {
  $special = '+^%~()[]'
  $out = New-Object Text.StringBuilder
  foreach ($ch in $Text.ToCharArray()) {
    switch ($ch) {
      "`r" { break }
      "`n" { [void] $out.Append('{ENTER}'); break }
      "`t" { [void] $out.Append('{TAB}'); break }
      '{' { [void] $out.Append('{{}'); break }
      '}' { [void] $out.Append('{}}'); break }
      default {
        if ($special.IndexOf($ch) -ge 0) { [void] $out.Append('{').Append($ch).Append('}') }
        else { [void] $out.Append($ch) }
      }
    }
  }
  $out.ToString()
}

function Get-ReturnMode($Opts) {
  $mode = (Str-Opt $Opts 'Return' 'restore').ToLowerInvariant()
  if (@('restore','keep') -notcontains $mode) { Fail 'invalid-value' 'Return must be restore or keep' 'restore puts the human back in their window when the action is done' }
  $mode
}

function Format-TakeoverLabel([string] $Action, $Target, $X = $null, $Y = $null) {
  $where = if ($Target) {
    $title = [string] $Target.title
    if ($title.Length -gt 60) { $title = $title.Substring(0, 57) + '...' }
    $Target.process + ': ' + $title
  } elseif ($null -ne $X) {
    "screen $X,$Y"
  } else {
    'the focused window'
  }
  'win32-drive ' + $Action + ' -> ' + $where
}

function Restore-Foreground($Previous, [string] $Mode) {
  if ($Mode -ne 'restore' -or $null -eq $Previous -or $Previous.hwnd -eq 0) { return $null }
  $handle = [IntPtr] $Previous.hwnd
  if ([Win32DriveNative]::GetForegroundWindow() -eq $handle) { return $Previous.hwnd }
  if (-not [Win32DriveNative]::IsWindow($handle)) { return $null }
  if ([Win32DriveNative]::Foreground($handle, 400) -or [Win32DriveNative]::ForegroundUnlocked($handle, 400)) { return $Previous.hwnd }
  $null
}

# One process start costs more than every action in it, so a whole interaction
# can be sent as an ordered step list with pauses between steps.
function Invoke-Batch($Opts) {
  $json = if ($Opts.ContainsKey('Json')) { Str-Opt $Opts 'Json' }
    elseif ($Opts.ContainsKey('File')) { Get-Content -Raw -LiteralPath (Str-Opt $Opts 'File') }
    else { [Console]::In.ReadToEnd() }
  if (-not $json -or $json.Trim().Length -eq 0) {
    Fail 'batch-empty' 'batch needs steps' 'pass -Json, -File, or pipe JSON on stdin: [{"cmd":"list"},{"cmd":"wait","ms":300}]'
  }
  # Assigning first matters: @(ConvertFrom-Json ...) wraps the whole array in one element.
  try {
    $parsed = ConvertFrom-Json $json
  } catch {
    Fail 'batch-invalid-json' ('Batch steps are not valid JSON: ' + $_.Exception.Message) 'shells mangle embedded quotes: prefer -File steps.json or pipe the JSON on stdin' @{ received = $json.Substring(0, [Math]::Min(200, $json.Length)) }
  }
  $steps = @($parsed)
  $continueOnError = $Opts.ContainsKey('ContinueOnError')
  $results = @()
  $failedAt = $null
  for ($index = 0; $index -lt $steps.Count; $index++) {
    $step = $steps[$index]
    $name = [string] $step.cmd
    $stepOpts = @{}
    foreach ($property in $step.PSObject.Properties) {
      if ($property.Name -in @('cmd','ms')) { continue }
      if ($property.Value -is [bool]) {
        if ($property.Value) { $stepOpts[$property.Name] = $true }
        continue
      }
      if ($script:win32DriveValueOptions -notcontains $property.Name -and $script:win32DriveFlagOptions -notcontains $property.Name) {
        Fail 'unknown-option' ("Unknown option in step " + $index + ": " + $property.Name) 'steps use the same option names as the command line'
      }
      $stepOpts[$property.Name] = [string] $property.Value
    }
    $stepWatch = [Diagnostics.Stopwatch]::StartNew()
    try {
      $payload = if ($name -eq 'wait') {
        $ms = if ($null -ne $step.ms) { [int] $step.ms } else { 250 }
        Start-Sleep -Milliseconds $ms
        [pscustomobject] [ordered] @{ ok = $true; waitedMs = $ms }
      } else {
        Invoke-DriveCommand $name $stepOpts
      }
      $results += (Add-Envelope $name $payload $index $stepWatch)
    } catch {
      $results += (Add-Envelope $name (Get-FailurePayload $name $_) $index $stepWatch)
      $failedAt = $index
      if (-not $continueOnError) { break }
    }
  }
  [pscustomobject] [ordered] @{
    ok = ($null -eq $failedAt)
    steps = $steps.Count
    ran = $results.Count
    failedAt = $failedAt
    results = $results
  }
}

function Add-Envelope([string] $Name, $Payload, $Step = $null, $Watch = $null) {
  $elapsed = if ($Watch) { [int] $Watch.ElapsedMilliseconds } else { [int] $script:win32DriveStart.ElapsedMilliseconds }
  $result = $Payload | Add-Member -PassThru -NotePropertyName cmd -NotePropertyValue $Name -Force
  $result = $result | Add-Member -PassThru -NotePropertyName elapsedMs -NotePropertyValue $elapsed -Force
  if ($null -ne $Step) { $result = $result | Add-Member -PassThru -NotePropertyName step -NotePropertyValue $Step -Force }
  $result
}

function Show-Help {
  [pscustomobject] [ordered] @{
    ok = $true
    runtime = 'Windows PowerShell 5.1+'
    dpiAwareness = $script:win32DriveDpi
    nativeLoad = $script:win32DriveNativeLoad
    speed = 'process start dominates: send one batch of steps instead of many invocations; every result carries cmd and elapsedMs'
    errors = 'failures are JSON with code, hint, and context: candidates for no-window/ambiguous-window/no-element, foreground plus lockTimeoutMs plus idleMs for focus-denied'
    takeover = 'pointer and focus commands show a named banner with a countdown; user input during the countdown aborts with code takeover-aborted'
    coordinates = 'screen pixels; with a window selector, click -X/-Y are relative to the DWM frame bounds reported by list and shot'
    racingGuard = 'racing commands fade a 16px red border/ring, wait for user idle, then act'
    focus = 'uia-invoke and uia-set act without focus, pointer, or keyboard; click/type/key focus the target after the racing guard and verify it, where require uses activation APIs only, auto adds a caption click and needs -Pointer, and skip does not focus'
    pointerConsent = 'synthetic mouse input requires an explicit -Pointer flag'
    globalOptions = '-IdleMs n (default 800) and -TimeoutMs n (default 10000) tune the racing guard'
    commands = [ordered] @{
      help = [ordered] @{ racing = $false; usage = 'help' }
      batch = [ordered] @{ racing = $null; usage = 'batch -File steps.json | batch (JSON on stdin) | batch -Json ''[{"cmd":"list"},{"cmd":"wait","ms":300}]'' [-ContinueOnError]' }
      list = [ordered] @{ racing = $false; usage = 'list [-Hwnd n|-Process p|-TitleContains t]' }
      shot = [ordered] @{ racing = $false; usage = 'shot [-Hwnd n|-Process p -TitleContains t|-X x -Y y -Width w -Height h] [-Out file.png]' }
      pixel = [ordered] @{ racing = $false; usage = 'pixel -X x -Y y' }
      uia = [ordered] @{ racing = $false; usage = 'uia [-Hwnd n|-Process p|-TitleContains t] [-Depth n]' }
      'uia-invoke' = [ordered] @{ racing = $true; usage = 'uia-invoke -Hwnd n|-Process p|-TitleContains t [-Name n] [-AutomationId a] [-ClassName c] [-ControlType t] [-Index i]' }
      'uia-set' = [ordered] @{ racing = $true; usage = 'uia-set -Hwnd n|-Process p|-TitleContains t -Value text [-Name n] [-AutomationId a] [-ClassName c] [-ControlType t] [-Index i]' }
      cursor = [ordered] @{ racing = $false; usage = 'cursor' }
      focus = [ordered] @{ racing = $false; usage = 'focus' }
      idle = [ordered] @{ racing = $false; usage = 'idle' }
      clipboard = [ordered] @{ racing = $null; usage = 'clipboard | clipboard -Set text' }
      theme = [ordered] @{ racing = $true; usage = 'theme | theme -Mode dark|light [-Scope apps|system|both] [-Refresh broadcast|full]' }
      overlay = [ordered] @{ racing = $false; usage = 'overlay [-X x -Y y] [-Text banner] [-DurationMs ms]' }
      click = [ordered] @{ racing = $true; takeover = $true; usage = 'click -Pointer [-Hwnd n|-Process p|-TitleContains t] [-X x -Y y] [-Button left|right] [-Count n] [-Focus auto|require|skip] [-Cursor restore|keep] [-Return restore|keep] [-CountdownMs n|-NoCountdown]' }
      type = [ordered] @{ racing = $true; takeover = $true; usage = 'type [-Hwnd n|-Process p|-TitleContains t] -Text text [-Focus auto|require|skip] [-Pointer] [-Method unicode|sendkeys] [-Return restore|keep] [-CountdownMs n|-NoCountdown]' }
      key = [ordered] @{ racing = $true; takeover = $true; usage = 'key [-Hwnd n|-Process p|-TitleContains t] -Keys "^s" [-Focus auto|require|skip] [-Pointer] [-Return restore|keep] [-CountdownMs n|-NoCountdown]' }
      window = [ordered] @{ racing = $true; usage = 'window -Action move|min|max|restore|close [-Hwnd n|-Process p|-TitleContains t] [-X x -Y y -Width w -Height h]' }
    }
  }
}

function Invoke-DriveCommand([string] $Name, $Opts) {
  $opts = $Opts
  switch ($Name.ToLowerInvariant()) {
    'help' { return Show-Help }
    'list' {
      $windows = if (Has-Selector $opts) { @(Select-Windows $opts) } else { @(Get-Windows) }
      return [pscustomobject] [ordered] @{
        ok = $true
        count = $windows.Count
        foreground = (Get-ForegroundInfo)
        idleMs = Get-IdleMs
        windows = $windows
      }
    }
    'idle' { return [pscustomobject] [ordered] @{ ok = $true; idleMs = Get-IdleMs } }
    'focus' { return (Get-ForegroundInfo) | Add-Member -PassThru -NotePropertyName ok -NotePropertyValue $true }
    'cursor' {
      $p = New-Object Win32DriveNative+POINT
      [void] [Win32DriveNative]::GetCursorPos([ref] $p)
      return [pscustomobject] [ordered] @{ ok = $true; x = $p.X; y = $p.Y }
    }
    'overlay' {
      $banner = Str-Opt $opts 'Text'
      $duration = Int-Opt $opts 'DurationMs' 650
      $completed = Show-Overlay (Int-Opt $opts 'X') (Int-Opt $opts 'Y') $banner $duration ($opts.ContainsKey('Pointer') -eq $false -and $null -ne $banner)
      return [pscustomobject] [ordered] @{ ok = $true; banner = $banner; durationMs = $duration; completed = $completed }
    }
    'pixel' {
      $x = Int-Opt $opts 'X'
      $y = Int-Opt $opts 'Y'
      if ($x -eq $null -or $y -eq $null) { throw 'pixel needs -X and -Y' }
      return Get-PixelColor $x $y
    }
    'shot' {
      $out = Str-Opt $opts 'Out'
      if (Has-Selector $opts) { return Capture-Window (Resolve-Window $opts) $out }
      Import-Forms
      $screen = [System.Windows.Forms.SystemInformation]::VirtualScreen
      $x = Int-Opt $opts 'X' $screen.Left
      $y = Int-Opt $opts 'Y' $screen.Top
      $w = Int-Opt $opts 'Width' $screen.Width
      $h = Int-Opt $opts 'Height' $screen.Height
      return Capture-Region $x $y $w $h $out
    }
    'uia-invoke' {
      $element = Resolve-UiaElement $opts
      $describe = Describe-UiaElement $element
      Invoke-RacingGuard $opts $describe.x $describe.y
      $pattern = Invoke-UiaElement $element
      return [pscustomobject] [ordered] @{ ok = $true; pattern = $pattern; element = $describe; foreground = (Get-ForegroundInfo) }
    }
    'uia-set' {
      if (-not $opts.ContainsKey('Value')) { throw 'uia-set needs -Value' }
      $element = Resolve-UiaElement $opts
      $describe = Describe-UiaElement $element
      Invoke-RacingGuard $opts $describe.x $describe.y
      $pattern = Set-UiaElementValue $element (Str-Opt $opts 'Value')
      return [pscustomobject] [ordered] @{ ok = $true; pattern = $pattern; element = $describe; foreground = (Get-ForegroundInfo) }
    }
    'uia' {
      Import-Uia
      $depth = Int-Opt $opts 'Depth' 2
      if (Has-Selector $opts) {
        $el = [Windows.Automation.AutomationElement]::FromHandle((Resolve-Window $opts))
      } else {
        $el = [Windows.Automation.AutomationElement]::FocusedElement
      }
      return [pscustomobject] [ordered] @{ ok = $true; tree = Dump-Uia $el 0 $depth }
    }
    'clipboard' {
      Import-Forms
      if ($opts.ContainsKey('Set')) {
        Invoke-RacingGuard $opts
        [Windows.Forms.Clipboard]::SetText((Str-Opt $opts 'Set'))
        return [pscustomobject] [ordered] @{ ok = $true; action = 'set' }
      }
      return [pscustomobject] [ordered] @{ ok = $true; text = [Windows.Forms.Clipboard]::GetText() }
    }
    'theme' {
      if (-not $opts.ContainsKey('Mode')) { return Get-Theme }
      Invoke-RacingGuard $opts
      return Set-Theme (Str-Opt $opts 'Mode') (Str-Opt $opts 'Scope' 'both') (Str-Opt $opts 'Refresh' 'broadcast')
    }
    'click' {
      if (-not $opts.ContainsKey('Pointer')) {
        Fail 'pointer-consent' 'click injects pointer input' 'ask the user first, then pass -Pointer; uia-invoke acts without pointer or focus'
      }
      $button = (Str-Opt $opts 'Button' 'left').ToLowerInvariant()
      $count = Int-Opt $opts 'Count' 1
      $focus = Get-FocusMode $opts
      $returnMode = Get-ReturnMode $opts
      $cursorMode = (Str-Opt $opts 'Cursor' 'restore').ToLowerInvariant()
      if (@('restore','keep') -notcontains $cursorMode) { Fail 'invalid-value' 'Cursor must be restore or keep' 'restore puts the pointer back where the human left it' }
      $x = Int-Opt $opts 'X'
      $y = Int-Opt $opts 'Y'
      $hwnd = $null
      $target = $null
      if (Has-Selector $opts) {
        $hwnd = Resolve-Window $opts
        $target = Get-WindowInfo $hwnd
        $r = Get-Rect $hwnd
        if ($x -eq $null) { $x = [int] (($r.Left + $r.Right) / 2) } else { $x = $r.Left + $x }
        if ($y -eq $null) { $y = [int] (($r.Top + $r.Bottom) / 2) } else { $y = $r.Top + $y }
      }
      if ($x -eq $null -or $y -eq $null) { Fail 'missing-option' 'click needs -X and -Y unless targeting a window center' 'pass screen coordinates, or a window selector for a center click' }
      $origin = New-Object Win32DriveNative+POINT
      [void] [Win32DriveNative]::GetCursorPos([ref] $origin)
      $previous = Get-ForegroundInfo
      # Focus after the guard: activation is only granted during input gaps.
      Invoke-Takeover $opts (Format-TakeoverLabel "$button click" $target $x $y) $x $y
      if ($hwnd -ne $null -and $focus -ne 'skip') { Set-Foreground $hwnd $focus }
      Invoke-PointerClick $x $y $button $count $false
      if ($cursorMode -eq 'restore') { [void] [Win32DriveNative]::SetCursorPos($origin.X, $origin.Y) }
      $returnedTo = Restore-Foreground $previous $returnMode
      return [pscustomobject] [ordered] @{
        ok = $true
        x = $x
        y = $y
        button = $button
        count = $count
        focus = $focus
        cursor = $cursorMode
        return = $returnMode
        returnedTo = $returnedTo
        target = $target
        foreground = (Get-ForegroundInfo)
      }
    }
    'type' {
      $text = Str-Opt $opts 'Text'
      if ($null -eq $text) { Fail 'missing-option' 'type needs -Text' 'pass the literal text; newlines become Enter' }
      $focus = Get-FocusMode $opts
      $returnMode = Get-ReturnMode $opts
      $handle = if (Has-Selector $opts) { Resolve-Window $opts } else { $null }
      $target = if ($handle) { Get-WindowInfo $handle } else { $null }
      $defaultMethod = if ([IntPtr]::Size -eq 8) { 'unicode' } else { 'sendkeys' }
      $method = (Str-Opt $opts 'Method' $defaultMethod).ToLowerInvariant()
      if (@('unicode','sendkeys') -notcontains $method) { Fail 'invalid-value' 'Method must be unicode or sendkeys' 'unicode sends the text verbatim; sendkeys uses the legacy escaped syntax' }
      $previous = Get-ForegroundInfo
      Invoke-Takeover $opts (Format-TakeoverLabel 'type' $target)
      if ($handle -ne $null -and $focus -ne 'skip') { Set-Foreground $handle $focus }
      if ($method -eq 'unicode') { [void] [Win32DriveNative]::TypeUnicode($text) }
      else { Import-Forms; [Windows.Forms.SendKeys]::SendWait((Escape-SendKeysText $text)) }
      $returnedTo = Restore-Foreground $previous $returnMode
      return [pscustomobject] [ordered] @{
        ok = $true
        chars = $text.Length
        method = $method
        focus = $focus
        return = $returnMode
        returnedTo = $returnedTo
        target = $target
        foreground = (Get-ForegroundInfo)
      }
    }
    'key' {
      $keySpec = Str-Opt $opts 'Keys'
      if (-not $keySpec) { Fail 'missing-option' 'key needs -Keys' 'SendKeys syntax, for example ^s or %{F4}' }
      $focus = Get-FocusMode $opts
      $returnMode = Get-ReturnMode $opts
      $handle = if (Has-Selector $opts) { Resolve-Window $opts } else { $null }
      $target = if ($handle) { Get-WindowInfo $handle } else { $null }
      $previous = Get-ForegroundInfo
      Invoke-Takeover $opts (Format-TakeoverLabel "key $keySpec" $target)
      if ($handle -ne $null -and $focus -ne 'skip') { Set-Foreground $handle $focus }
      Import-Forms
      [Windows.Forms.SendKeys]::SendWait($keySpec)
      $returnedTo = Restore-Foreground $previous $returnMode
      return [pscustomobject] [ordered] @{
        ok = $true
        keys = $keySpec
        focus = $focus
        return = $returnMode
        returnedTo = $returnedTo
        target = $target
        foreground = (Get-ForegroundInfo)
      }
    }
    'window' {
      $action = (Str-Opt $opts 'Action').ToLowerInvariant()
      if (-not $action) { Fail 'missing-option' 'window needs -Action' 'move, min, max, restore, or close' }
      $hwnd = Resolve-Window $opts
      Invoke-RacingGuard $opts
      switch ($action) {
        'move' {
          $r = Get-Rect $hwnd
          $x = Int-Opt $opts 'X' $r.Left
          $y = Int-Opt $opts 'Y' $r.Top
          $w = Int-Opt $opts 'Width' ($r.Right - $r.Left)
          $h = Int-Opt $opts 'Height' ($r.Bottom - $r.Top)
          [void] [Win32DriveNative]::MoveWindow($hwnd, $x, $y, $w, $h, $true)
        }
        'min' { [void] [Win32DriveNative]::ShowWindow($hwnd, 6) }
        'max' { [void] [Win32DriveNative]::ShowWindow($hwnd, 3) }
        'restore' { [void] [Win32DriveNative]::ShowWindow($hwnd, 9) }
        'close' { [void] [Win32DriveNative]::PostMessage($hwnd, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero) }
        default { Fail 'invalid-value' "Unknown window action: $action" 'move, min, max, restore, or close' }
      }
      Start-Sleep -Milliseconds 120
      return [pscustomobject] [ordered] @{
        ok = $true
        hwnd = $hwnd.ToInt64()
        action = $action
        window = (Get-WindowInfo $hwnd)
        foreground = (Get-ForegroundInfo)
      }
    }
    default {
      Fail 'unknown-command' "Unknown command: $Name" 'run help for the command catalog' @{ commands = @((Show-Help).commands.Keys) }
    }
  }
}

try {
  $options = Parse-Options $Rest
  $payload = if ($Command.ToLowerInvariant() -eq 'batch') { Invoke-Batch $options } else { Invoke-DriveCommand $Command $options }
  Write-Json (Add-Envelope $Command $payload)
  exit 0
} catch {
  Write-Json (Add-Envelope $Command (Get-FailurePayload $Command $_))
  exit 1
}