vdi-cloud/tools/cursor-poc-host.ps1

123 lines
7.3 KiB
PowerShell

# Scoped interactive cursor probe. No theme, driver, display or Sunshine changes.
$ErrorActionPreference='Stop'
$folder='C:\ProgramData\VDI\cursor-poc-20261103'
Set-Content "$folder\pid.txt" $PID
try {
Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class VDICursorShape {
public string name; public int width,height,hotX,hotY,invertedPixels;
public bool monochrome; public uint[] argb;
}
public class VDICursorProbe : Form {
[StructLayout(LayoutKind.Sequential)] struct POINT { public int x,y; }
[StructLayout(LayoutKind.Sequential)] struct CI { public int size,flags; public IntPtr cursor; public POINT point; }
[StructLayout(LayoutKind.Sequential)] struct II { public bool icon; public int x,y; public IntPtr mask,color; }
[DllImport("user32.dll")] static extern bool GetCursorInfo(ref CI ci);
[DllImport("user32.dll")] static extern bool GetIconInfo(IntPtr h,out II info);
[DllImport("user32.dll")] static extern bool DrawIconEx(IntPtr dc,int x,int y,IntPtr icon,int w,int h,int step,IntPtr brush,int flags);
[DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr h);
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
public static readonly string[] Names={"arrow","ibeam","sizewe","wait"};
static readonly Cursor[] Shapes={Cursors.Default,Cursors.IBeam,Cursors.SizeWE,Cursors.WaitCursor};
static Bitmap Draw(IntPtr handle,int w,int h,Color background) {
Bitmap result=new Bitmap(w,h,System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using(Graphics g=Graphics.FromImage(result)) {
g.Clear(background); IntPtr dc=g.GetHdc();
try { if(!DrawIconEx(dc,0,0,handle,w,h,0,IntPtr.Zero,3)) throw new Exception("DrawIconEx failed"); }
finally { g.ReleaseHdc(dc); }
}
return result;
}
public static VDICursorShape Export(string name,IntPtr handle) {
II info; if(!GetIconInfo(handle,out info)) throw new Exception("GetIconInfo failed");
try {
int w,h;
using(Bitmap mask=Bitmap.FromHbitmap(info.mask)) { w=mask.Width; h=mask.Height/(info.color==IntPtr.Zero?2:1); }
if(w<1 || h<1 || w>128 || h>128) throw new Exception("Unsupported cursor dimensions");
VDICursorShape s=new VDICursorShape {name=name,width=w,height=h,hotX=info.x,hotY=info.y,monochrome=info.color==IntPtr.Zero,argb=new uint[w*h]};
using(Bitmap black=Draw(handle,w,h,Color.Black))
using(Bitmap white=Draw(handle,w,h,Color.White)) {
for(int y=0;y<h;y++) for(int x=0;x<w;x++) {
Color b=black.GetPixel(x,y), z=white.GetPixel(x,y);
int dr=z.R-b.R,dg=z.G-b.G,db=z.B-b.B;
// XOR/inverted cursors need destination-dependent composition.
// Preserve the unsupported count; never silently call them alpha cursors.
if(dr<0 || dg<0 || db<0 || Math.Abs(dr-dg)>2 || Math.Abs(dr-db)>2) { ++s.invertedPixels; continue; }
int a=255-dr;
if(a==0) continue;
uint r=(uint)Math.Min(255,(b.R*255+a/2)/a),g=(uint)Math.Min(255,(b.G*255+a/2)/a),bl=(uint)Math.Min(255,(b.B*255+a/2)/a);
s.argb[y*w+x]=((uint)a<<24)|(r<<16)|(g<<8)|bl;
}
}
return s;
} finally { if(info.mask!=IntPtr.Zero) DeleteObject(info.mask); if(info.color!=IntPtr.Zero) DeleteObject(info.color); }
}
public static VDICursorShape[] ExportAll() {
List<VDICursorShape> result=new List<VDICursorShape>();
for(int i=0;i<Names.Length;i++) result.Add(Export(Names[i],Shapes[i].Handle));
return result.ToArray();
}
readonly Timer timer=new Timer(); readonly string folder; readonly DateTime started=DateTime.UtcNow;
bool hidden; string shape="arrow",lastObserved=""; int clicks;
public VDICursorProbe(string path) {
folder=path; Text="VDI Independent Cursor Probe"; FormBorderStyle=FormBorderStyle.None;
Bounds=Screen.PrimaryScreen.Bounds; TopMost=true; DoubleBuffered=true;
File.WriteAllText(Path.Combine(folder,"clicks.csv"),"utc,x,y,shape\n");
File.WriteAllText(Path.Combine(folder,"states.csv"),"utc,shape,flags,x,y\n");
timer.Interval=100;
timer.Tick+=delegate {
if((DateTime.UtcNow-started).TotalMinutes>8) { Close(); return; }
CI ci=new CI(); ci.size=Marshal.SizeOf(typeof(CI));
if(!GetCursorInfo(ref ci)) return;
string observed="unknown";
for(int i=0;i<Shapes.Length;i++) if(ci.cursor==Shapes[i].Handle) observed=Names[i];
string observedKey=observed+","+ci.flags;
if(observedKey!=lastObserved) {
File.AppendAllText(Path.Combine(folder,"states.csv"),DateTime.UtcNow.ToString("o")+","+observedKey+","+ci.point.x+","+ci.point.y+"\n");
lastObserved=observedKey;
}
string json="{\"version\":1,\"shape\":\""+observed+"\",\"visible\":"+(((ci.flags&1)!=0)?"true":"false")+",\"x\":"+ci.point.x+",\"y\":"+ci.point.y+",\"utc\":\""+DateTime.UtcNow.ToString("o")+"\"}";
string target=Path.Combine(folder,"state.json"),temp=target+".new";
File.WriteAllText(temp,json);
if(File.Exists(target)) File.Replace(temp,target,null); else File.Move(temp,target);
Invalidate(new Rectangle(0,0,Width,140));
};
timer.Start();
}
protected override void OnMouseMove(MouseEventArgs e) {
base.OnMouseMove(e);
bool nextHidden=e.Y>Height*3/4;
if(nextHidden!=hidden) { if(nextHidden) Cursor.Hide(); else Cursor.Show(); hidden=nextHidden; }
int index=Math.Min(3,e.X*4/Math.Max(1,Width)); shape=Names[index]; Cursor=Shapes[index];
}
protected override void OnMouseDown(MouseEventArgs e) {
base.OnMouseDown(e); ++clicks;
File.AppendAllText(Path.Combine(folder,"clicks.csv"),DateTime.UtcNow.ToString("o")+","+e.X+","+e.Y+","+shape+"\n"); Invalidate();
}
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.Clear(Color.FromArgb(24,32,44));
using(Font f=new Font("Consolas",23)) for(int i=0;i<4;i++) {
e.Graphics.DrawRectangle(Pens.SlateGray,i*Width/4,150,Width/4-2,Height*3/4-150);
e.Graphics.DrawString(Names[i],f,Brushes.White,i*Width/4+40,200);
}
using(Font f=new Font("Consolas",24)) {
e.Graphics.DrawString("Independent cursor probe | clicks: "+clicks+" | "+DateTime.UtcNow.ToString("HH:mm:ss.fff"),f,Brushes.White,40,40);
e.Graphics.DrawString("BOTTOM AREA: cursor hidden",f,Brushes.Gold,40,Height*3/4+60);
}
}
protected override void OnFormClosed(FormClosedEventArgs e) {
timer.Stop(); timer.Dispose(); if(hidden) { Cursor.Show(); hidden=false; } base.OnFormClosed(e);
}
}
'@
[VDICursorProbe]::SetProcessDPIAware() | Out-Null
@{version=1; shapes=[VDICursorProbe]::ExportAll()} | ConvertTo-Json -Depth 6 -Compress | Set-Content -Encoding utf8 "$folder\shapes.json"
[System.Windows.Forms.Application]::Run((New-Object VDICursorProbe($folder)))
} catch { $_ | Out-String | Set-Content "$folder\error.txt"; throw }