OpenScreen native cursor diagnostic
The red cross is the captured native hotspot. Native bitmaps are drawn at 1x, 2x, and 3x. The last cursor is a crisp vector 3x replacement anchored on the same hotspot.
import { spawn } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; function readPositiveIntEnv(name, fallback) { const raw = process.env[name]; if (raw === undefined) { return fallback; } const parsed = Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) { console.warn(`[cursor-native-test] ignoring invalid ${name}=${raw}; using ${fallback}`); return fallback; } return Math.floor(parsed); } const SAMPLE_INTERVAL_MS = readPositiveIntEnv("CURSOR_TEST_SAMPLE_INTERVAL_MS", 25); const DURATION_MS = readPositiveIntEnv("CURSOR_TEST_DURATION_MS", 1800); const SCREEN_FRAME_INTERVAL_MS = readPositiveIntEnv("CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS", 100); const READY_TIMEOUT_MS = readPositiveIntEnv("CURSOR_TEST_READY_TIMEOUT_MS", 5000); const OUTPUT_DIR = process.env.CURSOR_TEST_OUTPUT_DIR ?? path.join(os.tmpdir(), `openscreen-cursor-native-${Date.now()}`); if (process.platform !== "win32") { console.error("This diagnostic is Windows-only."); process.exit(1); } function encodePowerShell(script) { return Buffer.from(script, "utf16le").toString("base64"); } function quotePowerShellString(value) { return `'${String(value).replaceAll("'", "''")}'`; } function runPowerShell(script) { return new Promise((resolve, reject) => { const child = spawn( "powershell.exe", [ "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encodePowerShell(script), ], { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }, ); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => { stdout += chunk; }); child.stderr.on("data", (chunk) => { stderr += chunk; }); child.once("error", reject); child.once("exit", (code, signal) => { if (code === 0) { resolve(stdout); return; } reject( new Error(`PowerShell command failed (code=${code}, signal=${signal}): ${stderr.trim()}`), ); }); }); } function spawnPowerShell(script, { onStdout, onStderr } = {}) { const scriptPath = path.join( os.tmpdir(), `openscreen-powershell-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.ps1`, ); fs.writeFileSync(scriptPath, script, "utf8"); const child = spawn( "powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }, ); child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => onStdout?.(chunk)); child.stderr.on("data", (chunk) => onStderr?.(chunk)); const done = new Promise((resolve, reject) => { const cleanup = () => { fs.rmSync(scriptPath, { force: true }); }; child.once("error", (error) => { cleanup(); reject(error); }); child.once("exit", (code, signal) => { cleanup(); if (code === 0 || child.killed) { resolve({ code, signal }); return; } reject(new Error(`PowerShell process failed (code=${code}, signal=${signal})`)); }); }); return { child, done }; } function buildSamplerScript() { return String.raw` $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName System.Windows.Forms $source = @" using System; using System.Diagnostics; using System.Runtime.InteropServices; public static class OpenScreenCursorDiagnosticInterop { private const int WH_MOUSE_LL = 14; private const int WM_LBUTTONDOWN = 0x0201; private const int WM_LBUTTONUP = 0x0202; private static readonly object MouseSync = new object(); private static int LeftDownCount = 0; private static int LeftUpCount = 0; private static IntPtr MouseHook = IntPtr.Zero; private static LowLevelMouseProc MouseProcDelegate = MouseHookCallback; public delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam); public struct MouseButtonEvents { public int LeftDownCount; public int LeftUpCount; } [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; } [StructLayout(LayoutKind.Sequential)] public struct CURSORINFO { public int cbSize; public int flags; public IntPtr hCursor; public POINT ptScreenPos; } [StructLayout(LayoutKind.Sequential)] public struct ICONINFO { [MarshalAs(UnmanagedType.Bool)] public bool fIcon; public int xHotspot; public int yHotspot; public IntPtr hbmMask; public IntPtr hbmColor; } public static bool InstallMouseHook() { if (MouseHook != IntPtr.Zero) { return true; } using (Process process = Process.GetCurrentProcess()) using (ProcessModule module = process.MainModule) { MouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProcDelegate, GetModuleHandle(module.ModuleName), 0); } return MouseHook != IntPtr.Zero; } public static MouseButtonEvents ConsumeMouseButtonEvents() { lock (MouseSync) { MouseButtonEvents events = new MouseButtonEvents { LeftDownCount = LeftDownCount, LeftUpCount = LeftUpCount }; LeftDownCount = 0; LeftUpCount = 0; return events; } } private static IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { int message = wParam.ToInt32(); if (message == WM_LBUTTONDOWN || message == WM_LBUTTONUP) { lock (MouseSync) { if (message == WM_LBUTTONDOWN) { LeftDownCount += 1; } else { LeftUpCount += 1; } } } } return CallNextHookEx(MouseHook, nCode, wParam, lParam); } [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetCursorInfo(ref CURSORINFO pci); [DllImport("user32.dll")] public static extern short GetAsyncKeyState(int vKey); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr CopyIcon(IntPtr hIcon); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool DestroyIcon(IntPtr hIcon); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo); [DllImport("gdi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool DeleteObject(IntPtr hObject); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr GetModuleHandle(string lpModuleName); } "@ Add-Type -TypeDefinition $source $standardCursors = @{ arrow = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32512)) text = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32513)) wait = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32514)) crosshair = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32515)) 'up-arrow' = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32516)) 'resize-nwse' = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32642)) 'resize-nesw' = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32643)) 'resize-ew' = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32644)) 'resize-ns' = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32645)) move = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32646)) 'not-allowed' = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32648)) pointer = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32649)) 'app-starting' = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32650)) help = [OpenScreenCursorDiagnosticInterop]::LoadCursor([IntPtr]::Zero, [IntPtr]::new(32651)) } function Get-StandardCursorType($cursorHandle) { if ($cursorHandle -eq [IntPtr]::Zero) { return $null } foreach ($entry in $standardCursors.GetEnumerator()) { if ($entry.Value -eq $cursorHandle) { return $entry.Key } } return $null } function Write-JsonLine($payload) { [Console]::Out.WriteLine(($payload | ConvertTo-Json -Compress -Depth 6)) } function Get-CustomCursorType($bitmap, $hotspotX, $hotspotY) { if ($bitmap.Width -lt 24 -or $bitmap.Height -lt 24 -or $bitmap.Width -gt 64 -or $bitmap.Height -gt 64) { return $null } if ($hotspotX -lt ($bitmap.Width * 0.25) -or $hotspotX -gt ($bitmap.Width * 0.75) -or $hotspotY -lt ($bitmap.Height * 0.15) -or $hotspotY -gt ($bitmap.Height * 0.55)) { return $null } $opaquePixels = 0 $topHalfOpaquePixels = 0 $left = $bitmap.Width $top = $bitmap.Height $right = -1 $bottom = -1 for ($y = 0; $y -lt $bitmap.Height; $y++) { for ($x = 0; $x -lt $bitmap.Width; $x++) { if ($bitmap.GetPixel($x, $y).A -le 32) { continue } $opaquePixels += 1 if ($y -lt ($bitmap.Height / 2)) { $topHalfOpaquePixels += 1 } if ($x -lt $left) { $left = $x } if ($x -gt $right) { $right = $x } if ($y -lt $top) { $top = $y } if ($y -gt $bottom) { $bottom = $y } } } if ($opaquePixels -lt 90 -or $right -lt $left -or $bottom -lt $top) { return $null } $opaqueWidth = $right - $left + 1 $opaqueHeight = $bottom - $top + 1 if ($opaqueWidth -lt ($bitmap.Width * 0.35) -or $opaqueWidth -gt ($bitmap.Width * 0.9) -or $opaqueHeight -lt ($bitmap.Height * 0.45) -or $opaqueHeight -gt $bitmap.Height) { return $null } if ($top -gt ($bitmap.Height * 0.45) -or $bottom -lt ($bitmap.Height * 0.65)) { return $null } if ($topHalfOpaquePixels -gt ($opaquePixels * 0.55)) { return 'closed-hand' } return 'open-hand' } function Get-CursorAsset($cursorHandle, $cursorId) { $copiedHandle = [OpenScreenCursorDiagnosticInterop]::CopyIcon($cursorHandle) if ($copiedHandle -eq [IntPtr]::Zero) { return $null } $iconInfo = New-Object OpenScreenCursorDiagnosticInterop+ICONINFO $hasIconInfo = [OpenScreenCursorDiagnosticInterop]::GetIconInfo($copiedHandle, [ref]$iconInfo) try { $icon = [System.Drawing.Icon]::FromHandle($copiedHandle) $bitmap = New-Object System.Drawing.Bitmap $icon.Width, $icon.Height, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb) $graphics = [System.Drawing.Graphics]::FromImage($bitmap) $memoryStream = New-Object System.IO.MemoryStream try { $graphics.Clear([System.Drawing.Color]::Transparent) $graphics.DrawIcon($icon, 0, 0) $hotspotX = if ($hasIconInfo) { $iconInfo.xHotspot } else { 0 } $hotspotY = if ($hasIconInfo) { $iconInfo.yHotspot } else { 0 } $customCursorType = Get-CustomCursorType -bitmap $bitmap -hotspotX $hotspotX -hotspotY $hotspotY $bitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Png) $base64 = [System.Convert]::ToBase64String($memoryStream.ToArray()) return @{ id = $cursorId imageDataUrl = "data:image/png;base64,$base64" width = $bitmap.Width height = $bitmap.Height hotspotX = $hotspotX hotspotY = $hotspotY cursorType = $customCursorType } } finally { $memoryStream.Dispose() $graphics.Dispose() $bitmap.Dispose() $icon.Dispose() } } finally { if ($hasIconInfo) { if ($iconInfo.hbmMask -ne [IntPtr]::Zero) { [OpenScreenCursorDiagnosticInterop]::DeleteObject($iconInfo.hbmMask) | Out-Null } if ($iconInfo.hbmColor -ne [IntPtr]::Zero) { [OpenScreenCursorDiagnosticInterop]::DeleteObject($iconInfo.hbmColor) | Out-Null } } [OpenScreenCursorDiagnosticInterop]::DestroyIcon($copiedHandle) | Out-Null } } [OpenScreenCursorDiagnosticInterop]::InstallMouseHook() | Out-Null [OpenScreenCursorDiagnosticInterop]::GetAsyncKeyState(0x01) | Out-Null Write-JsonLine @{ type = 'ready'; timestampMs = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } $lastCursorId = $null $screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds while ($true) { [System.Windows.Forms.Application]::DoEvents() $mouseEvents = [OpenScreenCursorDiagnosticInterop]::ConsumeMouseButtonEvents() $cursorInfo = New-Object OpenScreenCursorDiagnosticInterop+CURSORINFO $cursorInfo.cbSize = [Runtime.InteropServices.Marshal]::SizeOf([type][OpenScreenCursorDiagnosticInterop+CURSORINFO]) if (-not [OpenScreenCursorDiagnosticInterop]::GetCursorInfo([ref]$cursorInfo)) { Write-JsonLine @{ type = 'error'; timestampMs = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(); message = 'GetCursorInfo failed' } Start-Sleep -Milliseconds ${SAMPLE_INTERVAL_MS} continue } $visible = ($cursorInfo.flags -band 1) -ne 0 $cursorId = if ($cursorInfo.hCursor -eq [IntPtr]::Zero) { $null } else { ('0x{0:X}' -f $cursorInfo.hCursor.ToInt64()) } $cursorType = Get-StandardCursorType $cursorInfo.hCursor $leftButtonState = [OpenScreenCursorDiagnosticInterop]::GetAsyncKeyState(0x01) $leftButtonDown = ($leftButtonState -band 0x8000) -ne 0 $leftButtonPressed = ($mouseEvents.LeftDownCount -gt 0) -or (($leftButtonState -band 0x0001) -ne 0) $leftButtonReleased = $mouseEvents.LeftUpCount -gt 0 $asset = $null if ($visible -and $cursorId -and $cursorId -ne $lastCursorId) { $asset = Get-CursorAsset -cursorHandle $cursorInfo.hCursor -cursorId $cursorId if ($asset -and $cursorType) { $asset.cursorType = $cursorType } elseif ($asset -and $asset.cursorType) { $cursorType = $asset.cursorType } $lastCursorId = $cursorId } Write-JsonLine @{ type = 'sample' timestampMs = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() x = $cursorInfo.ptScreenPos.X y = $cursorInfo.ptScreenPos.Y visible = $visible handle = $cursorId cursorType = $cursorType leftButtonDown = $leftButtonDown leftButtonPressed = $leftButtonPressed leftButtonReleased = $leftButtonReleased bounds = @{ x = $screenBounds.Left y = $screenBounds.Top width = $screenBounds.Width height = $screenBounds.Height } asset = $asset } Start-Sleep -Milliseconds ${SAMPLE_INTERVAL_MS} } `; } function buildMousePathScript(durationMs) { const stepMs = 120; const steps = Math.max(8, Math.floor(durationMs / stepMs)); return String.raw` $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Windows.Forms $source = @" using System.Runtime.InteropServices; using System; public static class OpenScreenMouseDiagnosticInterop { [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool SetCursorPos(int X, int Y); [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo); } "@ Add-Type -TypeDefinition $source $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds $points = @() for ($i = 0; $i -lt ${steps}; $i++) { $t = if (${steps} -le 1) { 0 } else { $i / (${steps} - 1) } $x = [int]($bounds.Left + 80 + (($bounds.Width - 160) * $t)) $wave = [Math]::Sin($t * [Math]::PI * 2) $y = [int]($bounds.Top + ($bounds.Height / 2) + ($wave * [Math]::Min(180, $bounds.Height / 4))) $points += @{ x = $x; y = $y } } for ($i = 0; $i -lt $points.Count; $i++) { $point = $points[$i] [OpenScreenMouseDiagnosticInterop]::SetCursorPos($point.x, $point.y) | Out-Null if ($i -eq [int]([Math]::Floor($points.Count / 2))) { [OpenScreenMouseDiagnosticInterop]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero) Start-Sleep -Milliseconds 12 [OpenScreenMouseDiagnosticInterop]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero) } Start-Sleep -Milliseconds ${stepMs} } `; } function buildScreenRecorderScript(outputDir, durationMs) { const framesDir = path.join(outputDir, "screen-frames"); return String.raw` $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName System.Windows.Forms $framesDir = ${quotePowerShellString(framesDir)} New-Item -ItemType Directory -Force -Path $framesDir | Out-Null $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds $targetWidth = 960 $targetHeight = [int]([Math]::Round($targetWidth * ($bounds.Height / $bounds.Width))) $frames = New-Object System.Collections.Generic.List[object] $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $index = 0 while ($stopwatch.ElapsedMilliseconds -le ${durationMs + 700}) { $sourceBitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb) $graphics = [System.Drawing.Graphics]::FromImage($sourceBitmap) $scaledBitmap = New-Object System.Drawing.Bitmap $targetWidth, $targetHeight, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb) $scaledGraphics = [System.Drawing.Graphics]::FromImage($scaledBitmap) $timestampMs = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() $fileName = ('frame_{0:D4}.png' -f $index) $path = Join-Path $framesDir $fileName try { $graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size) $scaledGraphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic $scaledGraphics.DrawImage($sourceBitmap, 0, 0, $targetWidth, $targetHeight) $scaledBitmap.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) $frames.Add(@{ index = $index timestampMs = $timestampMs path = $path width = $targetWidth height = $targetHeight bounds = @{ x = $bounds.Left y = $bounds.Top width = $bounds.Width height = $bounds.Height } }) | Out-Null } finally { $scaledGraphics.Dispose() $scaledBitmap.Dispose() $graphics.Dispose() $sourceBitmap.Dispose() } $index += 1 Start-Sleep -Milliseconds ${SCREEN_FRAME_INTERVAL_MS} } ($frames | ConvertTo-Json -Depth 6) | Set-Content -Path (Join-Path $framesDir 'frames.json') -Encoding UTF8 `; } function createReadyWaiter() { let settled = false; let resolveReady = null; const promise = new Promise((resolve, reject) => { const timer = setTimeout(() => { if (settled) { return; } settled = true; reject(new Error("Timed out waiting for cursor sampler readiness.")); }, READY_TIMEOUT_MS); resolveReady = () => { if (settled) { return; } settled = true; clearTimeout(timer); resolve(); }; }); return { promise, resolve: () => resolveReady?.(), }; } function writeAssets(assets, outputDir) { const assetDir = path.join(outputDir, "assets"); fs.mkdirSync(assetDir, { recursive: true }); for (const asset of assets.values()) { const base64 = asset.imageDataUrl?.replace(/^data:image\/png;base64,/, ""); if (!base64) { continue; } const safeId = String(asset.id).replace(/[^a-zA-Z0-9_-]/g, "_"); fs.writeFileSync(path.join(assetDir, `${safeId}.png`), Buffer.from(base64, "base64")); } } function toRecordingData(samples, assets) { const firstTimestampMs = samples[0]?.timestampMs ?? Date.now(); let previousLeftButtonDown = false; const normalizedSamples = samples.flatMap((sample) => { const bounds = sample.bounds; if (!bounds || bounds.width <= 0 || bounds.height <= 0) { return []; } const leftButtonDown = sample.leftButtonDown === true; const leftButtonPressed = sample.leftButtonPressed === true; const leftButtonReleased = sample.leftButtonReleased === true; const interactionType = leftButtonPressed || (leftButtonDown && !previousLeftButtonDown) ? "click" : leftButtonReleased || (!leftButtonDown && previousLeftButtonDown) ? "mouseup" : "move"; previousLeftButtonDown = leftButtonDown; return [ { timeMs: Math.max(0, sample.timestampMs - firstTimestampMs), cx: (sample.x - bounds.x) / bounds.width, cy: (sample.y - bounds.y) / bounds.height, assetId: sample.handle, visible: Boolean(sample.visible), cursorType: sample.cursorType ?? null, interactionType, }, ]; }); return { version: 2, provider: assets.size > 0 ? "native" : "none", samples: normalizedSamples, assets: [...assets.values()].map((asset) => ({ id: asset.id, platform: "win32", imageDataUrl: asset.imageDataUrl, width: asset.width, height: asset.height, hotspotX: asset.hotspotX, hotspotY: asset.hotspotY, scaleFactor: 1, cursorType: asset.cursorType ?? null, })), }; } function escapeScriptJson(value) { return JSON.stringify(value).replace(/
The red cross is the captured native hotspot. Native bitmaps are drawn at 1x, 2x, and 3x. The last cursor is a crisp vector 3x replacement anchored on the same hotspot.
Background frames are real Windows screenshots. Native bitmaps are reconstructed at 1x, 2x, and 3x; the last cursor is a crisp vector 3x replacement. The red cross marks the recorded hotspot.