Files
pebrel/scripts/ui_probe.ps1
T
kuddev 7ef5b8ccf9 feat: workspace export/import with split-tree session restore; fix oversized startup window
Session v4 doubles as the workspace file format: tabs persist their full
split tree, per-pane cwds and launch identity (shell/profile/SSH), so both
crash recovery and exported workspaces rebuild real layouts.

Startup now always sizes the window from the config base font at the
standard 116x30 grid: the persisted Ctrl+wheel zoom no longer inflates the
first frame, and the saved window size/maximized state is write-only.
2026-07-25 21:33:49 +08:00

125 lines
5.8 KiB
PowerShell

# Interactive UI probe pinned to a specific nebula PID so the user's own
# instance is never touched. Uses PrintWindow so screenshots capture the target
# window even when it is occluded. Clicks require foreground; retry + verify.
param(
[int]$ProcId = 0,
[string]$Click = "",
[string]$RightClick = "",
[int]$Scroll = 0,
[int]$CtrlScroll = 0,
[string]$Shot = "",
[string]$TypeText = "",
[switch]$Kill
)
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class W {
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")] public static extern void mouse_event(uint f, int dx, int dy, uint d, UIntPtr e);
[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr dc, uint flags);
[DllImport("user32.dll")] public static extern uint GetDpiForWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
public struct RECT { public int L, T, R, B; }
}
'@
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# Physical-pixel truth: without this the probe process is DPI-unaware and
# PrintWindow/GetWindowRect return virtualized (shrunken) coordinates —
# screenshots silently lie about glyph sizes on high-DPI monitors.
Add-Type -MemberDefinition '[DllImport("shcore.dll")] public static extern int SetProcessDpiAwareness(int value);' -Name Dpi -Namespace W2
[void][W2.Dpi]::SetProcessDpiAwareness(2)
if ($ProcId -eq 0) {
$p = Start-Process "D:\temp_build\nebula\target\release\nebula.exe" -ArgumentList '--working-directory','D:\temp_build' -PassThru
Start-Sleep -Seconds 6
$p.Refresh()
Write-Output "started pid=$($p.Id)"
$ProcId = $p.Id
}
$proc = Get-Process -Id $ProcId -ErrorAction SilentlyContinue
if (-not $proc) { Write-Output "PROCESS $ProcId GONE"; exit 1 }
if ($Kill) { $proc.Kill(); Write-Output "killed $ProcId"; exit 0 }
$h = $proc.MainWindowHandle
if ($h -eq 0) { Write-Output "NO WINDOW for $ProcId"; exit 1 }
$r = New-Object W+RECT
[void][W]::GetWindowRect($h, [ref]$r)
Write-Output "pid=$ProcId rect=($($r.L),$($r.T),$($r.R),$($r.B)) size=$($r.R-$r.L)x$($r.B-$r.T) dpi=$([W]::GetDpiForWindow($h))"
if ($Click -ne "" -or $RightClick -ne "" -or $Scroll -ne 0 -or $CtrlScroll -ne 0 -or $TypeText -ne "") {
# Activation needed for input. Retry until we really own the foreground.
for ($i = 0; $i -lt 5; $i++) {
[void][W]::ShowWindow($h, 9) # SW_RESTORE
[void][W]::SetForegroundWindow($h)
Start-Sleep -Milliseconds 400
if ([W]::GetForegroundWindow() -eq $h) { break }
}
if ([W]::GetForegroundWindow() -ne $h) { Write-Output "FOREGROUND FAILED"; exit 1 }
}
if ($Click -ne "") {
$parts = $Click.Split(','); $cx = [int]$parts[0] + $r.L; $cy = [int]$parts[1] + $r.T
Write-Output "click client($($parts[0]),$($parts[1])) -> screen($cx,$cy)"
[void][W]::SetCursorPos($cx, $cy); Start-Sleep -Milliseconds 250
[W]::mouse_event(2,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 60
[W]::mouse_event(4,0,0,0,[UIntPtr]::Zero)
Start-Sleep -Milliseconds 700
}
if ($RightClick -ne "") {
$parts = $RightClick.Split(','); $cx = [int]$parts[0] + $r.L; $cy = [int]$parts[1] + $r.T
Write-Output "right-click client($($parts[0]),$($parts[1])) -> screen($cx,$cy)"
[void][W]::SetCursorPos($cx, $cy); Start-Sleep -Milliseconds 250
[W]::mouse_event(8,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 60
[W]::mouse_event(16,0,0,0,[UIntPtr]::Zero)
Start-Sleep -Milliseconds 700
}
if ($Scroll -ne 0) {
$sx = [int]($r.L + ($r.R - $r.L) * 0.6); $sy = [int]($r.T + ($r.B - $r.T) * 0.5)
[void][W]::SetCursorPos($sx, $sy); Start-Sleep -Milliseconds 150
[W]::mouse_event(0x0800, 0, 0, [uint32]($Scroll * 120), [UIntPtr]::Zero)
Start-Sleep -Milliseconds 500
}
if ($CtrlScroll -ne 0) {
# Ctrl held around the wheel ticks: terminal font zoom. The key-up runs
# in `finally` — a mid-loop error must NEVER strand a system-wide
# synthetic Ctrl (it turns every later wheel into font zoom).
$sx = [int]($r.L + ($r.R - $r.L) * 0.6); $sy = [int]($r.T + ($r.B - $r.T) * 0.5)
[void][W]::SetCursorPos($sx, $sy); Start-Sleep -Milliseconds 150
[W]::keybd_event(0x11, 0, 0, [UIntPtr]::Zero) # VK_CONTROL down
try {
Start-Sleep -Milliseconds 80
$ticks = [Math]::Abs($CtrlScroll); $dir = [Math]::Sign($CtrlScroll)
# Negative wheel deltas must go through a bit-pattern cast: [uint32](-120) throws.
$delta = [BitConverter]::ToUInt32([BitConverter]::GetBytes([int]($dir * 120)), 0)
for ($i = 0; $i -lt $ticks; $i++) {
[W]::mouse_event(0x0800, 0, 0, $delta, [UIntPtr]::Zero)
Start-Sleep -Milliseconds 120
}
} finally {
[W]::keybd_event(0x11, 0, 2, [UIntPtr]::Zero) # VK_CONTROL up (KEYEVENTF_KEYUP)
}
Start-Sleep -Milliseconds 500
}
if ($TypeText -ne "") {
[System.Windows.Forms.SendKeys]::SendWait($TypeText)
Start-Sleep -Milliseconds 500
}
if ($Shot -ne "") {
$wr = New-Object W+RECT
[void][W]::GetWindowRect($h, [ref]$wr)
$w = $wr.R - $wr.L; $ht = $wr.B - $wr.T
$bmp = New-Object System.Drawing.Bitmap($w, $ht)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$dc = $g.GetHdc()
# PW_RENDERFULLCONTENT (2) captures GPU-composited windows.
[void][W]::PrintWindow($h, $dc, 2)
$g.ReleaseHdc($dc)
$bmp.Save($Shot, [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose(); $bmp.Dispose()
Write-Output "shot=$Shot"
}