Capture-MonitorLayout.ps1
1 #!/usr/bin/env pwsh 2 <# 3 .SYNOPSIS 4 Captures the current monitor layout configuration for CursorWrap testing. 5 6 .DESCRIPTION 7 Queries Windows for all connected monitors and saves their configuration 8 (position, size, DPI, primary status) to a JSON file that can be used 9 for testing the CursorWrap module. 10 11 By default, potentially identifying information (computer name, user name, 12 device names) is anonymized to protect privacy when sharing layout files. 13 14 .PARAMETER OutputPath 15 Path where the JSON file will be saved. Default: cursorwrap_monitor_layout.json 16 17 .PARAMETER AddUserMachineNames 18 Include computer name and user name in the output. By default these are 19 blank to protect privacy when sharing layout files. 20 21 .PARAMETER AddDeviceNames 22 Include device names (e.g., \\.\DISPLAY1) in the output. By default these 23 are anonymized to "DISPLAY1", "DISPLAY2", etc. to reduce fingerprinting. 24 25 .PARAMETER Help 26 Show this help message and exit. 27 28 .EXAMPLE 29 .\Capture-MonitorLayout.ps1 30 Captures layout with privacy-safe defaults (no user/machine names). 31 32 .EXAMPLE 33 .\Capture-MonitorLayout.ps1 -OutputPath "my_setup.json" 34 Saves to a custom filename. 35 36 .EXAMPLE 37 .\Capture-MonitorLayout.ps1 -AddUserMachineNames 38 Includes computer name and user name in the output. 39 40 .EXAMPLE 41 .\Capture-MonitorLayout.ps1 -AddUserMachineNames -AddDeviceNames 42 Includes all identifying information (useful for personal debugging). 43 #> 44 45 param( 46 [Parameter(Mandatory=$false)] 47 [string]$OutputPath = "cursorwrap_monitor_layout.json", 48 49 [Parameter(Mandatory=$false)] 50 [switch]$AddUserMachineNames, 51 52 [Parameter(Mandatory=$false)] 53 [switch]$AddDeviceNames, 54 55 [Parameter(Mandatory=$false)] 56 [Alias("h", "?")] 57 [switch]$Help 58 ) 59 60 # Show help if requested 61 if ($Help) { 62 Get-Help $MyInvocation.MyCommand.Path -Detailed 63 exit 0 64 } 65 66 # Add Windows Forms for screen enumeration 67 Add-Type -AssemblyName System.Windows.Forms 68 69 function Get-MonitorDPI { 70 param([System.Windows.Forms.Screen]$Screen) 71 72 # Try to get DPI using P/Invoke with multiple methods 73 Add-Type @" 74 using System; 75 using System.Runtime.InteropServices; 76 public class DisplayConfig { 77 [DllImport("user32.dll")] 78 public static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags); 79 80 [DllImport("shcore.dll")] 81 public static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY); 82 83 [DllImport("user32.dll")] 84 public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi); 85 86 [DllImport("user32.dll")] 87 public static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags); 88 89 [DllImport("gdi32.dll")] 90 public static extern int GetDeviceCaps(IntPtr hdc, int nIndex); 91 92 [DllImport("user32.dll")] 93 public static extern IntPtr GetDC(IntPtr hWnd); 94 95 [DllImport("user32.dll")] 96 public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); 97 98 [StructLayout(LayoutKind.Sequential)] 99 public struct POINT { 100 public int X; 101 public int Y; 102 } 103 104 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] 105 public struct MONITORINFOEX { 106 public int cbSize; 107 public RECT rcMonitor; 108 public RECT rcWork; 109 public uint dwFlags; 110 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] 111 public string szDevice; 112 } 113 114 [StructLayout(LayoutKind.Sequential)] 115 public struct RECT { 116 public int Left; 117 public int Top; 118 public int Right; 119 public int Bottom; 120 } 121 122 public const uint MONITOR_DEFAULTTOPRIMARY = 1; 123 public const int MDT_EFFECTIVE_DPI = 0; 124 public const int MDT_ANGULAR_DPI = 1; 125 public const int MDT_RAW_DPI = 2; 126 public const int LOGPIXELSX = 88; 127 public const int LOGPIXELSY = 90; 128 } 129 "@ -ErrorAction SilentlyContinue 130 131 try { 132 $point = New-Object DisplayConfig+POINT 133 $point.X = $Screen.Bounds.Left + ($Screen.Bounds.Width / 2) 134 $point.Y = $Screen.Bounds.Top + ($Screen.Bounds.Height / 2) 135 136 $hMonitor = [DisplayConfig]::MonitorFromPoint($point, 1) 137 138 # Method 1: Try GetDpiForMonitor (Windows 8.1+) 139 [uint]$dpiX = 0 140 [uint]$dpiY = 0 141 $result = [DisplayConfig]::GetDpiForMonitor($hMonitor, 0, [ref]$dpiX, [ref]$dpiY) 142 143 if ($result -eq 0 -and $dpiX -gt 0) { 144 Write-Verbose "DPI detected via GetDpiForMonitor: $dpiX" 145 return $dpiX 146 } 147 148 # Method 2: Try RAW DPI 149 $result = [DisplayConfig]::GetDpiForMonitor($hMonitor, 2, [ref]$dpiX, [ref]$dpiY) 150 if ($result -eq 0 -and $dpiX -gt 0) { 151 Write-Verbose "DPI detected via RAW DPI: $dpiX" 152 return $dpiX 153 } 154 155 # Method 3: Try getting device context DPI (legacy method) 156 $hdc = [DisplayConfig]::GetDC([IntPtr]::Zero) 157 if ($hdc -ne [IntPtr]::Zero) { 158 $dpiValue = [DisplayConfig]::GetDeviceCaps($hdc, 88) # LOGPIXELSX 159 [DisplayConfig]::ReleaseDC([IntPtr]::Zero, $hdc) 160 if ($dpiValue -gt 0) { 161 Write-Verbose "DPI detected via GetDeviceCaps: $dpiValue" 162 return $dpiValue 163 } 164 } 165 } 166 catch { 167 Write-Verbose "DPI detection error: $($_.Exception.Message)" 168 } 169 170 Write-Warning "Could not detect DPI for $($Screen.DeviceName), using default 96 DPI" 171 return 96 # Standard 96 DPI (100% scaling) 172 } 173 174 function Capture-MonitorLayout { 175 Write-Host "Capturing monitor layout..." -ForegroundColor Cyan 176 Write-Host "=" * 80 177 178 $screens = [System.Windows.Forms.Screen]::AllScreens 179 $monitors = @() 180 $monitorIndex = 1 181 182 foreach ($screen in $screens) { 183 $isPrimary = $screen.Primary 184 $bounds = $screen.Bounds 185 $dpi = Get-MonitorDPI -Screen $screen 186 187 # Anonymize device name by default to reduce fingerprinting 188 $deviceName = if ($AddDeviceNames) { 189 $screen.DeviceName 190 } else { 191 "DISPLAY$monitorIndex" 192 } 193 194 $monitor = [ordered]@{ 195 left = $bounds.Left 196 top = $bounds.Top 197 right = $bounds.Right 198 bottom = $bounds.Bottom 199 width = $bounds.Width 200 height = $bounds.Height 201 dpi = $dpi 202 scaling_percent = [math]::Round(($dpi / 96.0) * 100, 0) 203 primary = $isPrimary 204 device_name = $deviceName 205 } 206 207 $monitors += $monitor 208 $monitorIndex++ 209 210 # Display info 211 $primaryTag = if ($isPrimary) { " [PRIMARY]" } else { "" } 212 $scaling = [math]::Round(($dpi / 96.0) * 100, 0) 213 214 Write-Host "`nMonitor $($monitors.Count)$primaryTag" -ForegroundColor Green 215 Write-Host " Device: $($screen.DeviceName)" 216 Write-Host " Position: ($($bounds.Left), $($bounds.Top))" 217 Write-Host " Size: $($bounds.Width)x$($bounds.Height)" 218 Write-Host " DPI: $dpi ($scaling% scaling)" 219 Write-Host " Bounds: [$($bounds.Left), $($bounds.Top), $($bounds.Right), $($bounds.Bottom)]" 220 } 221 222 # Create output object with privacy-safe defaults 223 $output = [ordered]@{ 224 captured_at = (Get-Date -Format "yyyy-MM-ddTHH:mm:sszzz") 225 computer_name = if ($AddUserMachineNames) { $env:COMPUTERNAME } else { "" } 226 user_name = if ($AddUserMachineNames) { $env:USERNAME } else { "" } 227 monitor_count = $monitors.Count 228 monitors = $monitors 229 } 230 231 # Save to JSON 232 $json = $output | ConvertTo-Json -Depth 10 233 Set-Content -Path $OutputPath -Value $json -Encoding UTF8 234 235 Write-Host "`n" + ("=" * 80) 236 Write-Host "Monitor layout saved to: $OutputPath" -ForegroundColor Green 237 Write-Host "Total monitors captured: $($monitors.Count)" -ForegroundColor Cyan 238 Write-Host "`nYou can now use this file with the test script:" -ForegroundColor Yellow 239 Write-Host " python monitor_layout_tests.py --layout-file $OutputPath" -ForegroundColor White 240 241 return $output 242 } 243 244 # Main execution 245 try { 246 $layout = Capture-MonitorLayout 247 248 # Display summary 249 Write-Host "`n" + ("=" * 80) 250 Write-Host "SUMMARY" -ForegroundColor Cyan 251 Write-Host ("=" * 80) 252 if ($layout.computer_name) { 253 Write-Host "Configuration Name: $($layout.computer_name)" 254 } 255 Write-Host "Captured: $($layout.captured_at)" 256 Write-Host "Monitors: $($layout.monitor_count)" 257 258 # Privacy notice 259 if (-not $AddUserMachineNames -or -not $AddDeviceNames) { 260 Write-Host "`nPrivacy: " -NoNewline -ForegroundColor Yellow 261 $privacyNotes = @() 262 if (-not $AddUserMachineNames) { $privacyNotes += "user/machine names excluded" } 263 if (-not $AddDeviceNames) { $privacyNotes += "device names anonymized" } 264 Write-Host ($privacyNotes -join ", ") -ForegroundColor Yellow 265 Write-Host " Use -AddUserMachineNames and/or -AddDeviceNames to include." -ForegroundColor DarkGray 266 } 267 268 # Calculate desktop dimensions 269 $widths = @($layout.monitors | ForEach-Object { $_.width }) 270 $heights = @($layout.monitors | ForEach-Object { $_.height }) 271 272 $totalWidth = ($widths | Measure-Object -Sum).Sum 273 $maxHeight = ($heights | Measure-Object -Maximum).Maximum 274 275 Write-Host "Total Desktop Width: $totalWidth pixels" 276 Write-Host "Max Desktop Height: $maxHeight pixels" 277 278 # Analyze potential coordinate issues 279 Write-Host "`n" + ("=" * 80) 280 Write-Host "COORDINATE ANALYSIS" -ForegroundColor Cyan 281 Write-Host ("=" * 80) 282 283 # Check for gaps between monitors 284 if ($layout.monitor_count -gt 1) { 285 $hasGaps = $false 286 for ($i = 0; $i -lt $layout.monitor_count - 1; $i++) { 287 $m1 = $layout.monitors[$i] 288 for ($j = $i + 1; $j -lt $layout.monitor_count; $j++) { 289 $m2 = $layout.monitors[$j] 290 291 # Check horizontal gap 292 $hGap = [Math]::Min([Math]::Abs($m1.right - $m2.left), [Math]::Abs($m2.right - $m1.left)) 293 # Check vertical overlap 294 $vOverlapStart = [Math]::Max($m1.top, $m2.top) 295 $vOverlapEnd = [Math]::Min($m1.bottom, $m2.bottom) 296 $vOverlap = $vOverlapEnd - $vOverlapStart 297 298 if ($hGap -gt 50 -and $vOverlap -gt 0) { 299 Write-Host "⚠ Gap detected between Monitor $($i+1) and Monitor $($j+1): ${hGap}px horizontal gap" -ForegroundColor Yellow 300 Write-Host " Vertical overlap: ${vOverlap}px" -ForegroundColor Yellow 301 Write-Host " This may indicate a Windows coordinate bug if monitors appear snapped in Display Settings" -ForegroundColor Yellow 302 $hasGaps = $true 303 } 304 } 305 } 306 if (-not $hasGaps) { 307 Write-Host "✓ No unexpected gaps detected" -ForegroundColor Green 308 } 309 } 310 311 # DPI/Scaling notes 312 Write-Host "`nDPI/Scaling Impact on Coordinates:" -ForegroundColor Cyan 313 Write-Host "• Coordinate values (left, top, right, bottom) are in LOGICAL PIXELS" 314 Write-Host "• These are DPI-independent virtual coordinates" 315 Write-Host "• Physical pixels = Logical pixels × (DPI / 96)" 316 Write-Host "• Example: 1920 logical pixels at 150% scaling = 1920 × 1.5 = 2880 physical pixels" 317 Write-Host "• Windows snaps monitors using logical pixel coordinates" 318 Write-Host "• If monitors appear snapped but coordinates show gaps, this is a Windows bug" 319 320 exit 0 321 } 322 catch { 323 Write-Host "`nError capturing monitor layout:" -ForegroundColor Red 324 Write-Host $_.Exception.Message -ForegroundColor Red 325 Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray 326 exit 1 327 }