Files
code-utils/tools/drive.ps1

176 lines
7.2 KiB
PowerShell
Raw Normal View History

2026-08-14 07:52:01 +08:00
# drive.ps1 - focus the Code Count window, optionally send keys, optionally capture a screenshot.
# Usage: powershell -File drive.ps1 -Keys "alt right right down enter" -Out shot.png -SettleMs 800
param(
[string]$Keys = "",
[string]$Out = "",
[string]$RClick = "", # "x,y" window-relative right click
[string]$LClick = "", # "x,y" window-relative left click
[string]$Move = "", # "x,y" window-relative hover (no click)
[string]$Wheel = "", # "x,y,ticks" wheel at point (positive ticks = up)
[int]$SettleMs = 700,
[int]$KeyGapMs = 150,
[long]$Hwnd = 0
)
$ErrorActionPreference = 'Stop'
Add-Type -ReferencedAssemblies System.Drawing @"
using System;
using System.Runtime.InteropServices;
public class Drv {
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr dc, uint flags);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint fl, UIntPtr ex);
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
[DllImport("user32.dll")] public static extern void mouse_event(uint fl, int dx, int dy, uint data, UIntPtr ex);
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string cls, string title);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
[DllImport("user32.dll")] public static extern IntPtr WindowFromPoint(POINT p);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassName(IntPtr h, System.Text.StringBuilder sb, int n);
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L; public int T; public int R; public int B; }
}
"@
[Drv]::SetProcessDPIAware() | Out-Null
if ($Hwnd -ne 0) {
$h = [IntPtr]$Hwnd
} else {
# Window title is non-ASCII now; resolve via process main window handle to keep this file ASCII-safe.
$proc = Get-Process code-count | Where-Object { $_.MainWindowHandle -ne 0 } | Sort-Object StartTime -Descending | Select-Object -First 1
if (-not $proc) { throw "app window not found" }
$h = $proc.MainWindowHandle
}
if ([Drv]::IsIconic($h)) {
[Drv]::ShowWindow($h, 9) | Out-Null # SW_RESTORE
Start-Sleep -Milliseconds 600
Write-Output "restored from minimized"
}
[Drv]::SetForegroundWindow($h) | Out-Null
Start-Sleep -Milliseconds 300
if ([Drv]::GetForegroundWindow() -ne $h) {
# press Alt to satisfy the foreground-lock heuristic, then retry
[Drv]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero)
[Drv]::SetForegroundWindow($h) | Out-Null
[Drv]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
Start-Sleep -Milliseconds 300
}
Write-Output ("foreground=" + ([Drv]::GetForegroundWindow() -eq $h))
Start-Sleep -Milliseconds 200
$vk = @{ alt=0x12; ctrl=0x11; shift=0x10; enter=0x0D; esc=0x1B; tab=0x09; space=0x20; f10=0x79;
left=0x25; up=0x26; right=0x27; down=0x28; apps=0x5D }
$ext = @(0x25,0x26,0x27,0x28,0x5D)
function Press([int[]]$codes) {
foreach ($c in $codes) {
$f = if ($ext -contains $c) { 1 } else { 0 }
[Drv]::keybd_event([byte]$c, 0, [uint32]$f, [UIntPtr]::Zero)
Start-Sleep -Milliseconds 25
}
[array]::Reverse($codes)
foreach ($c in $codes) {
$f = if ($ext -contains $c) { 3 } else { 2 }
[Drv]::keybd_event([byte]$c, 0, [uint32]$f, [UIntPtr]::Zero)
Start-Sleep -Milliseconds 25
}
}
if ($Keys) {
foreach ($tok in $Keys.Split(' ')) {
if (-not $tok) { continue }
if ($tok.StartsWith('type:')) {
foreach ($ch in $tok.Substring(5).ToCharArray()) { Press @([int][char]::ToUpper($ch)) }
continue
}
$codes = @()
foreach ($part in $tok.Split('+')) {
if ($vk.ContainsKey($part)) { $codes += [int]$vk[$part] }
elseif ($part.Length -eq 1) { $codes += [int][char]::ToUpper($part[0]) }
else { throw "unknown key: $part" }
}
Press $codes
Start-Sleep -Milliseconds $KeyGapMs
}
}
if ($Move) {
$xy = $Move.Split(',')
$rm = New-Object Drv+RECT
[Drv]::GetWindowRect($h, [ref]$rm) | Out-Null
[Drv]::SetCursorPos($rm.L + [int]$xy[0], $rm.T + [int]$xy[1]) | Out-Null
# nudge one pixel so the webview registers a mousemove event
Start-Sleep -Milliseconds 80
[Drv]::mouse_event(0x0001, 1, 0, 0, [UIntPtr]::Zero)
Start-Sleep -Milliseconds 200
}
if ($RClick) {
$xy = $RClick.Split(',')
$r0 = New-Object Drv+RECT
[Drv]::GetWindowRect($h, [ref]$r0) | Out-Null
$px = $r0.L + [int]$xy[0]; $py = $r0.T + [int]$xy[1]
[Drv]::SetCursorPos($px, $py) | Out-Null
Start-Sleep -Milliseconds 150
[Drv]::mouse_event(0x0008, 0, 0, 0, [UIntPtr]::Zero) # right down
Start-Sleep -Milliseconds 60
[Drv]::mouse_event(0x0010, 0, 0, 0, [UIntPtr]::Zero) # right up
Start-Sleep -Milliseconds 300
}
if ($LClick) {
$xy = $LClick.Split(',')
$r1 = New-Object Drv+RECT
[Drv]::GetWindowRect($h, [ref]$r1) | Out-Null
$tx = $r1.L + [int]$xy[0]; $ty = $r1.T + [int]$xy[1]
[Drv]::SetCursorPos($tx - 1, $ty) | Out-Null
Start-Sleep -Milliseconds 80
[Drv]::mouse_event(0x0001, 1, 0, 0, [UIntPtr]::Zero) # real WM_MOUSEMOVE for the webview
Start-Sleep -Milliseconds 120
$cp = New-Object Drv+POINT
[Drv]::GetCursorPos([ref]$cp) | Out-Null
$wfp = [Drv]::WindowFromPoint($cp)
$wpid = 0
[Drv]::GetWindowThreadProcessId($wfp, [ref]$wpid) | Out-Null
$wcls = New-Object System.Text.StringBuilder 128
[Drv]::GetClassName($wfp, $wcls, 128) | Out-Null
Write-Output "rect=($($r1.L),$($r1.T))-($($r1.R),$($r1.B)) target=($tx,$ty) cursor=($($cp.X),$($cp.Y)) under=$wfp cls=$($wcls.ToString()) pid=$wpid"
[Drv]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero) # left down
Start-Sleep -Milliseconds 60
[Drv]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero) # left up
Start-Sleep -Milliseconds 300
}
if ($Wheel) {
$wp = $Wheel.Split(',')
$r2 = New-Object Drv+RECT
[Drv]::GetWindowRect($h, [ref]$r2) | Out-Null
[Drv]::SetCursorPos($r2.L + [int]$wp[0], $r2.T + [int]$wp[1]) | Out-Null
Start-Sleep -Milliseconds 120
$ticks = [int]$wp[2]
$dir = if ($ticks -gt 0) { 120 } else { -120 }
for ($i = 0; $i -lt [Math]::Abs($ticks); $i++) {
[Drv]::mouse_event(0x0800, 0, 0, [uint32]($dir -band 0xFFFFFFFF), [UIntPtr]::Zero)
Start-Sleep -Milliseconds 60
}
}
if ($Out) {
Start-Sleep -Milliseconds $SettleMs
$r = New-Object Drv+RECT
[Drv]::GetWindowRect($h, [ref]$r) | Out-Null
$w = $r.R - $r.L; $ht = $r.B - $r.T
$bmp = New-Object System.Drawing.Bitmap($w, $ht)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$dc = $g.GetHdc()
[Drv]::PrintWindow($h, $dc, 2) | Out-Null
$g.ReleaseHdc($dc); $g.Dispose()
$bmp.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
Write-Output "saved $Out ($w x $ht)"
}