Win32 input emits Esc down+up with uChar=27 so Claude Code sees 0x1b; kitty flags encode CSI 27u instead of a bare escape. The TABS plus button now creates the configured default shell; the dots keep the profile picker. Plus/dots use the old 20px chrome size. Checkpoint remaining GPUI parity work in the tree (settings rewrite, math overlay, SSH settings, SVN status).
161 lines
7.6 KiB
PowerShell
161 lines
7.6 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);
|
|
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
|
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
public delegate bool EnumProc(IntPtr h, IntPtr lparam);
|
|
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr lparam);
|
|
// Process.MainWindowHandle picks whatever top-level window answers first --
|
|
// for nebula that is a 6x6 helper window (and a minimized one reports
|
|
// -32000,-32000). Enumerate instead and keep the largest visible window
|
|
// owned by the pid: that is the real shell window in both shells.
|
|
public static IntPtr FindMainWindow(int pid) {
|
|
IntPtr best = IntPtr.Zero;
|
|
long bestArea = 0;
|
|
EnumWindows(delegate(IntPtr h, IntPtr l) {
|
|
uint wpid;
|
|
GetWindowThreadProcessId(h, out wpid);
|
|
if (wpid != (uint)pid) return true;
|
|
if (!IsWindowVisible(h)) return true;
|
|
RECT r;
|
|
GetWindowRect(h, out r);
|
|
long area = (long)(r.R - r.L) * (long)(r.B - r.T);
|
|
// Minimized windows sit at -32000; never let one win the comparison.
|
|
if (r.L <= -30000 || r.T <= -30000) return true;
|
|
if (area > bestArea) { bestArea = area; best = h; }
|
|
return true;
|
|
}, IntPtr.Zero);
|
|
return best;
|
|
}
|
|
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 = [W]::FindMainWindow($ProcId)
|
|
if ($h -eq [IntPtr]::Zero) {
|
|
# Fall back to the (unreliable) handle so a minimized-only state still
|
|
# reports something actionable instead of looking like a dead process.
|
|
$h = $proc.MainWindowHandle
|
|
if ($h -eq 0) { Write-Output "NO WINDOW for $ProcId"; exit 1 }
|
|
Write-Output "WARN: no visible top-level window; falling back to MainWindowHandle"
|
|
}
|
|
$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
|
|
# WHEEL_DELTA is signed (negative scrolls down) but the P/Invoke signature
|
|
# declares dwData as uint: casting a negative int straight to [uint32]
|
|
# throws InvalidCast. Mask to the two's-complement bit pattern instead.
|
|
$wheel = [uint32]([int64]($Scroll * 120) -band 0xFFFFFFFFL)
|
|
[W]::mouse_event(0x0800, 0, 0, $wheel, [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"
|
|
}
|