Skip to content

Instantly share code, notes, and snippets.

@jeffreyschultz
Last active September 9, 2025 01:58
Show Gist options
  • Select an option

  • Save jeffreyschultz/e4b6c583b86517ab6ddbfe3f7064be56 to your computer and use it in GitHub Desktop.

Select an option

Save jeffreyschultz/e4b6c583b86517ab6ddbfe3f7064be56 to your computer and use it in GitHub Desktop.
#Requires -Version 5.1
<#
.SYNOPSIS
Off-Screen Window Recovery Module
.DESCRIPTION
A PowerShell module that provides functionality to detect and recover windows that have
become positioned off-screen, typically after disconnecting external monitors or changing
display configurations. This is especially useful for picture-in-picture windows,
floating toolbars, and other windows that can get "lost" outside the visible desktop area.
.AUTHOR
Jeffrey Schultz <jeffrey@schultz.mx>
.VERSION
1.0.0
.NOTES
This module uses Windows API calls to enumerate and reposition windows.
Requires Windows operating system.
.EXAMPLE
Import-Module .\OffScreenWindowRecovery.psm1
Repair-OffScreenWindows
.EXAMPLE
Import-Module .\OffScreenWindowRecovery.psm1
Get-OffScreenWindows -Verbose
Repair-OffScreenWindows -ProcessName "vivaldi" -Verbose
#>
# Windows API definitions - only add if not already defined
if (-not ('WindowAPI' -as [type])) {
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using System.Text;
public struct RECT {
public int left;
public int top;
public int right;
public int bottom;
}
public struct POINT {
public int x;
public int y;
}
public static class WindowAPI {
[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
public static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern int GetSystemMetrics(int nIndex);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
// SetWindowPos flags
public const uint SWP_NOSIZE = 0x0001;
public const uint SWP_NOZORDER = 0x0004;
public const uint SWP_SHOWWINDOW = 0x0040;
public const uint SWP_FRAMECHANGED = 0x0020;
// ShowWindow commands
public const int SW_RESTORE = 9;
public const int SW_SHOW = 5;
// System metrics
public const int SM_CXSCREEN = 0;
public const int SM_CYSCREEN = 1;
public const int SM_CXVIRTUALSCREEN = 78;
public const int SM_CYVIRTUALSCREEN = 79;
public const int SM_XVIRTUALSCREEN = 76;
public const int SM_YVIRTUALSCREEN = 77;
}
"@
}
# Internal helper function to get screen information
function Get-ScreenInformation {
[CmdletBinding()]
param()
try {
# Try to use Windows Forms for accurate screen info
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
$primaryScreen = [System.Windows.Forms.Screen]::PrimaryScreen
$allScreens = [System.Windows.Forms.Screen]::AllScreens
$screenInfo = @{
Primary = @{
Width = $primaryScreen.Bounds.Width
Height = $primaryScreen.Bounds.Height
Left = $primaryScreen.Bounds.Left
Top = $primaryScreen.Bounds.Top
}
Virtual = @{
Width = ($allScreens | Measure-Object -Property {$_.Bounds.Width} -Sum).Sum
Height = ($allScreens | Measure-Object -Property {$_.Bounds.Height} -Maximum).Maximum
Left = ($allScreens | Measure-Object -Property {$_.Bounds.Left} -Minimum).Minimum
Top = ($allScreens | Measure-Object -Property {$_.Bounds.Top} -Minimum).Minimum
}
AllScreens = $allScreens | ForEach-Object {
@{
Width = $_.Bounds.Width
Height = $_.Bounds.Height
Left = $_.Bounds.Left
Top = $_.Bounds.Top
IsPrimary = $_.Primary
DeviceName = $_.DeviceName
}
}
}
}
catch {
# Fallback to Windows API
$screenInfo = @{
Primary = @{
Width = [WindowAPI]::GetSystemMetrics([WindowAPI]::SM_CXSCREEN)
Height = [WindowAPI]::GetSystemMetrics([WindowAPI]::SM_CYSCREEN)
Left = 0
Top = 0
}
Virtual = @{
Width = [WindowAPI]::GetSystemMetrics([WindowAPI]::SM_CXVIRTUALSCREEN)
Height = [WindowAPI]::GetSystemMetrics([WindowAPI]::SM_CYVIRTUALSCREEN)
Left = [WindowAPI]::GetSystemMetrics([WindowAPI]::SM_XVIRTUALSCREEN)
Top = [WindowAPI]::GetSystemMetrics([WindowAPI]::SM_YVIRTUALSCREEN)
}
AllScreens = @(
@{
Width = [WindowAPI]::GetSystemMetrics([WindowAPI]::SM_CXSCREEN)
Height = [WindowAPI]::GetSystemMetrics([WindowAPI]::SM_CYSCREEN)
Left = 0
Top = 0
IsPrimary = $true
DeviceName = "Primary"
}
)
}
}
return $screenInfo
}
function Get-OffScreenWindows {
<#
.SYNOPSIS
Gets a list of all windows that are currently positioned off-screen.
.DESCRIPTION
Enumerates all visible windows and identifies those that are positioned outside
the current visible desktop area. This includes windows that are completely
off-screen or mostly off-screen (with only a small portion visible).
.PARAMETER ProcessName
Optional filter to only check windows from processes matching this name.
Supports wildcards. Example: "chrome", "vivaldi", "notepad"
.PARAMETER IncludeMinimized
Include minimized windows in the search. By default, minimized windows are ignored.
.PARAMETER Tolerance
Number of pixels a window can be off-screen before being considered "off-screen".
Default is 100 pixels to allow for partially visible windows.
.EXAMPLE
Get-OffScreenWindows
Returns all off-screen windows.
.EXAMPLE
Get-OffScreenWindows -ProcessName "vivaldi"
Returns only Vivaldi browser windows that are off-screen.
.EXAMPLE
Get-OffScreenWindows -Verbose -Tolerance 50
Returns off-screen windows with detailed information and custom tolerance.
#>
[CmdletBinding()]
param(
[string]$ProcessName = "",
[switch]$IncludeMinimized,
[int]$Tolerance = 100
)
$screenInfo = Get-ScreenInformation
$primaryScreen = $screenInfo.Primary
Write-Verbose "Primary screen: $($primaryScreen.Width)x$($primaryScreen.Height) at ($($primaryScreen.Left), $($primaryScreen.Top))"
# Get target process IDs if filtering
$targetProcessIds = @()
if ($ProcessName) {
$processes = Get-Process | Where-Object { $_.ProcessName -like "*$ProcessName*" }
$targetProcessIds = $processes | Select-Object -ExpandProperty Id
Write-Verbose "Found $($targetProcessIds.Count) processes matching '$ProcessName'"
}
$offScreenWindows = @()
# Enumerate all windows
$enumCallback = {
param($hWnd, $lParam)
try {
$processId = 0
[WindowAPI]::GetWindowThreadProcessId($hWnd, [ref]$processId)
# Filter by process if specified
if ($ProcessName -and $targetProcessIds -notcontains $processId) {
return $true
}
# Check if window is visible
if (-not [WindowAPI]::IsWindowVisible($hWnd)) {
return $true
}
# Check if window is minimized (skip unless explicitly requested)
if (-not $IncludeMinimized -and [WindowAPI]::IsIconic($hWnd)) {
return $true
}
# Get window rectangle
$rect = New-Object RECT
[WindowAPI]::GetWindowRect($hWnd, [ref]$rect)
# Get window title
$length = [WindowAPI]::GetWindowTextLength($hWnd)
$title = ""
if ($length -gt 0) {
$sb = New-Object System.Text.StringBuilder($length + 1)
[WindowAPI]::GetWindowText($hWnd, $sb, $sb.Capacity)
$title = $sb.ToString()
}
# Calculate window dimensions
$windowWidth = $rect.right - $rect.left
$windowHeight = $rect.bottom - $rect.top
# Skip very small windows (likely system windows) or windows without titles
if ($windowWidth -lt 50 -or $windowHeight -lt 50 -or $title.Length -eq 0) {
return $true
}
# Check if window is off-screen
$isOffScreen = (
$rect.left -lt ($primaryScreen.Left - $Tolerance) -or
$rect.top -lt ($primaryScreen.Top - $Tolerance) -or
$rect.right -gt ($primaryScreen.Left + $primaryScreen.Width + $Tolerance) -or
$rect.bottom -gt ($primaryScreen.Top + $primaryScreen.Height + $Tolerance) -or
$rect.right -lt ($primaryScreen.Left + $Tolerance) -or
$rect.bottom -lt ($primaryScreen.Top + $Tolerance)
)
if ($isOffScreen) {
# Get process information
$process = Get-Process -Id $processId -ErrorAction SilentlyContinue
$processName = if ($process) { $process.ProcessName } else { "Unknown" }
$windowInfo = [PSCustomObject]@{
Handle = $hWnd
ProcessId = $processId
ProcessName = $processName
Title = $title
Left = $rect.left
Top = $rect.top
Right = $rect.right
Bottom = $rect.bottom
Width = $windowWidth
Height = $windowHeight
IsMinimized = [WindowAPI]::IsIconic($hWnd)
OffScreenReason = @()
}
# Determine why it's considered off-screen
if ($rect.left -lt ($primaryScreen.Left - $Tolerance)) {
$windowInfo.OffScreenReason += "Left edge off-screen"
}
if ($rect.top -lt ($primaryScreen.Top - $Tolerance)) {
$windowInfo.OffScreenReason += "Top edge off-screen"
}
if ($rect.right -gt ($primaryScreen.Left + $primaryScreen.Width + $Tolerance)) {
$windowInfo.OffScreenReason += "Right edge off-screen"
}
if ($rect.bottom -gt ($primaryScreen.Top + $primaryScreen.Height + $Tolerance)) {
$windowInfo.OffScreenReason += "Bottom edge off-screen"
}
if ($rect.right -lt ($primaryScreen.Left + $Tolerance)) {
$windowInfo.OffScreenReason += "Completely left of screen"
}
if ($rect.bottom -lt ($primaryScreen.Top + $Tolerance)) {
$windowInfo.OffScreenReason += "Completely above screen"
}
$offScreenWindows += $windowInfo
}
}
catch {
Write-Verbose "Error processing window: $($_.Exception.Message)"
}
return $true
}
# Execute the enumeration
[WindowAPI]::EnumWindows($enumCallback, [IntPtr]::Zero) | Out-Null
Write-Verbose "Found $($offScreenWindows.Count) off-screen windows"
return $offScreenWindows
}
function Repair-OffScreenWindows {
<#
.SYNOPSIS
Brings all off-screen windows back onto the current desktop.
.DESCRIPTION
This function finds all windows that are positioned outside the visible screen area
and moves them back to a visible location. Useful when windows get stuck off-screen
after disconnecting external monitors or changing display configurations.
.PARAMETER ProcessName
Optional. Filter windows by process name (e.g., 'vivaldi', 'chrome', 'notepad').
If not specified, all off-screen windows will be moved.
.PARAMETER IncludeMinimized
Include minimized windows in the recovery process. By default, minimized windows are ignored.
.PARAMETER Tolerance
Number of pixels a window can be off-screen before being considered "off-screen".
Default is 100 pixels to allow for partially visible windows.
.PARAMETER Position
Where to position recovered windows. Options: 'Cascade', 'TopLeft', 'Center', 'TopRight'
Default is 'Cascade' which positions windows in a cascading pattern.
.PARAMETER RestoreMinimized
If true, minimized off-screen windows will be restored (un-minimized) when moved.
.PARAMETER BringToFront
If true, recovered windows will be brought to the foreground.
.EXAMPLE
Repair-OffScreenWindows
Moves all off-screen windows back to the visible desktop using cascade positioning.
.EXAMPLE
Repair-OffScreenWindows -ProcessName vivaldi -Verbose
Moves only Vivaldi browser windows that are off-screen with detailed output.
.EXAMPLE
Repair-OffScreenWindows -Position Center -BringToFront
Centers all off-screen windows and brings them to the front.
.EXAMPLE
Repair-OffScreenWindows -IncludeMinimized -RestoreMinimized
Includes minimized windows and restores them when moving.
#>
[CmdletBinding()]
param(
[string]$ProcessName = "",
[switch]$IncludeMinimized,
[int]$Tolerance = 100,
[ValidateSet('Cascade', 'TopLeft', 'Center', 'TopRight')]
[string]$Position = 'Cascade',
[switch]$RestoreMinimized,
[switch]$BringToFront
)
# Get off-screen windows
$offScreenWindows = Get-OffScreenWindows -ProcessName $ProcessName -IncludeMinimized:$IncludeMinimized -Tolerance $Tolerance -Verbose:$VerbosePreference
if ($offScreenWindows.Count -eq 0) {
Write-Host "No off-screen windows found." -ForegroundColor Green
return @()
}
Write-Host "Found $($offScreenWindows.Count) off-screen window$(if ($offScreenWindows.Count -ne 1) {'s'}):" -ForegroundColor Yellow
$screenInfo = Get-ScreenInformation
$primaryScreen = $screenInfo.Primary
$movedWindows = @()
$windowsMoved = 0
foreach ($window in $offScreenWindows) {
try {
Write-Verbose "Processing window: '$($window.Title)' (PID: $($window.ProcessId))"
# Calculate new position based on positioning strategy
switch ($Position) {
'TopLeft' {
$newX = $primaryScreen.Left + 50
$newY = $primaryScreen.Top + 50
}
'Center' {
$newX = $primaryScreen.Left + ($primaryScreen.Width - $window.Width) / 2
$newY = $primaryScreen.Top + ($primaryScreen.Height - $window.Height) / 2
}
'TopRight' {
$newX = $primaryScreen.Left + $primaryScreen.Width - $window.Width - 50
$newY = $primaryScreen.Top + 50
}
'Cascade' {
$newX = $primaryScreen.Left + 50 + ($windowsMoved * 30)
$newY = $primaryScreen.Top + 50 + ($windowsMoved * 30)
}
}
# Ensure window fits on screen
$newX = [Math]::Max($primaryScreen.Left, [Math]::Min($newX, $primaryScreen.Left + $primaryScreen.Width - $window.Width))
$newY = [Math]::Max($primaryScreen.Top, [Math]::Min($newY, $primaryScreen.Top + $primaryScreen.Height - $window.Height))
# Additional bounds checking
if ($newX + $window.Width -gt $primaryScreen.Left + $primaryScreen.Width) {
$newX = $primaryScreen.Left + $primaryScreen.Width - $window.Width - 10
}
if ($newY + $window.Height -gt $primaryScreen.Top + $primaryScreen.Height) {
$newY = $primaryScreen.Top + $primaryScreen.Height - $window.Height - 10
}
# Ensure coordinates are not negative
$newX = [Math]::Max($primaryScreen.Left, $newX)
$newY = [Math]::Max($primaryScreen.Top, $newY)
Write-Verbose "Moving '$($window.Title)' from ($($window.Left), $($window.Top)) to ($newX, $newY)"
# Restore minimized window if requested
if ($window.IsMinimized -and $RestoreMinimized) {
[WindowAPI]::ShowWindow($window.Handle, [WindowAPI]::SW_RESTORE) | Out-Null
Write-Verbose "Restored minimized window"
}
# Move the window
$success = [WindowAPI]::SetWindowPos(
$window.Handle,
[IntPtr]::Zero,
[int]$newX,
[int]$newY,
0,
0,
[WindowAPI]::SWP_NOSIZE -bor [WindowAPI]::SWP_NOZORDER -bor [WindowAPI]::SWP_SHOWWINDOW
)
if ($success) {
$windowsMoved++
# Bring to front if requested
if ($BringToFront) {
[WindowAPI]::SetForegroundWindow($window.Handle) | Out-Null
}
$movedWindow = $window.PSObject.Copy()
$movedWindow | Add-Member -NotePropertyName NewLeft -NotePropertyValue $newX
$movedWindow | Add-Member -NotePropertyName NewTop -NotePropertyValue $newY
$movedWindow | Add-Member -NotePropertyName MoveSuccessful -NotePropertyValue $true
$movedWindows += $movedWindow
Write-Host " ✓ Moved: $($window.Title)" -ForegroundColor Green
if ($VerbosePreference -eq 'Continue') {
Write-Host " Process: $($window.ProcessName) (PID: $($window.ProcessId))" -ForegroundColor Gray
Write-Host " Reason: $($window.OffScreenReason -join ', ')" -ForegroundColor Gray
Write-Host " Position: ($($window.Left), $($window.Top)) → ($newX, $newY)" -ForegroundColor Gray
}
}
else {
Write-Host " ✗ Failed to move: $($window.Title)" -ForegroundColor Red
$failedWindow = $window.PSObject.Copy()
$failedWindow | Add-Member -NotePropertyName MoveSuccessful -NotePropertyValue $false
$failedWindow | Add-Member -NotePropertyName FailureReason -NotePropertyValue "SetWindowPos failed"
$movedWindows += $failedWindow
}
}
catch {
Write-Host " ✗ Error moving window '$($window.Title)': $($_.Exception.Message)" -ForegroundColor Red
$failedWindow = $window.PSObject.Copy()
$failedWindow | Add-Member -NotePropertyName MoveSuccessful -NotePropertyValue $false
$failedWindow | Add-Member -NotePropertyName FailureReason -NotePropertyValue $_.Exception.Message
$movedWindows += $failedWindow
}
}
# Summary
$successCount = ($movedWindows | Where-Object { $_.MoveSuccessful }).Count
$failCount = ($movedWindows | Where-Object { -not $_.MoveSuccessful }).Count
Write-Host ""
Write-Host "Recovery Summary:" -ForegroundColor Cyan
Write-Host " Successfully moved: $successCount" -ForegroundColor Green
Write-Host " Failed to move: $failCount" -ForegroundColor $(if ($failCount -gt 0) { 'Red' } else { 'Gray' })
Write-Host " Total processed: $($movedWindows.Count)" -ForegroundColor White
return $movedWindows
}
function Show-ScreenInformation {
<#
.SYNOPSIS
Displays information about the current screen configuration.
.DESCRIPTION
Shows details about the primary screen and all connected displays,
including dimensions and positions. Useful for troubleshooting
off-screen window issues.
.EXAMPLE
Show-ScreenInformation
Displays current screen configuration.
#>
[CmdletBinding()]
param()
$screenInfo = Get-ScreenInformation
Write-Host "Screen Configuration:" -ForegroundColor Cyan
Write-Host ""
Write-Host "Primary Screen:" -ForegroundColor Yellow
Write-Host " Dimensions: $($screenInfo.Primary.Width) x $($screenInfo.Primary.Height)" -ForegroundColor White
Write-Host " Position: ($($screenInfo.Primary.Left), $($screenInfo.Primary.Top))" -ForegroundColor White
Write-Host ""
if ($screenInfo.AllScreens.Count -gt 1) {
Write-Host "All Screens:" -ForegroundColor Yellow
for ($i = 0; $i -lt $screenInfo.AllScreens.Count; $i++) {
$screen = $screenInfo.AllScreens[$i]
$isPrimary = if ($screen.IsPrimary) { " (Primary)" } else { "" }
Write-Host " Screen $($i + 1)$isPrimary : $($screen.Width) x $($screen.Height) at ($($screen.Left), $($screen.Top))" -ForegroundColor White
if ($screen.DeviceName) {
Write-Host " Device: $($screen.DeviceName)" -ForegroundColor Gray
}
}
Write-Host ""
}
Write-Host "Virtual Desktop:" -ForegroundColor Yellow
Write-Host " Total area: $($screenInfo.Virtual.Width) x $($screenInfo.Virtual.Height)" -ForegroundColor White
Write-Host " Top-left: ($($screenInfo.Virtual.Left), $($screenInfo.Virtual.Top))" -ForegroundColor White
}
# Export module members
Export-ModuleMember -Function @(
'Get-OffScreenWindows',
'Repair-OffScreenWindows',
'Show-ScreenInformation'
)
# Create aliases for common usage patterns - avoid conflicts during reloading
if (-not (Get-Alias 'Fix-Windows' -ErrorAction SilentlyContinue)) {
New-Alias -Name 'Fix-Windows' -Value 'Repair-OffScreenWindows' -Description "Alias for Repair-OffScreenWindows"
}
if (-not (Get-Alias 'Bring-Windows' -ErrorAction SilentlyContinue)) {
New-Alias -Name 'Bring-Windows' -Value 'Repair-OffScreenWindows' -Description "Alias for Repair-OffScreenWindows"
}
if (-not (Get-Alias 'Recover-Windows' -ErrorAction SilentlyContinue)) {
New-Alias -Name 'Recover-Windows' -Value 'Repair-OffScreenWindows' -Description "Alias for Repair-OffScreenWindows"
}
if (-not (Get-Alias 'Find-OffScreenWindows' -ErrorAction SilentlyContinue)) {
New-Alias -Name 'Find-OffScreenWindows' -Value 'Get-OffScreenWindows' -Description "Alias for Get-OffScreenWindows"
}
if (-not (Get-Alias 'Screen-Info' -ErrorAction SilentlyContinue)) {
New-Alias -Name 'Screen-Info' -Value 'Show-ScreenInformation' -Description "Alias for Show-ScreenInformation"
}
Export-ModuleMember -Alias @(
'Fix-Windows',
'Bring-Windows',
'Recover-Windows',
'Find-OffScreenWindows',
'Screen-Info'
)
# Module initialization message
Write-Host "Off-Screen Window Recovery Module loaded!" -ForegroundColor Green
Write-Host "Available commands:" -ForegroundColor Yellow
Write-Host " • Repair-OffScreenWindows (Fix-Windows)" -ForegroundColor White
Write-Host " • Get-OffScreenWindows (Find-OffScreenWindows)" -ForegroundColor White
Write-Host " • Show-ScreenInformation (Screen-Info)" -ForegroundColor White
Write-Host ""
Write-Host "Quick start: Run 'Fix-Windows' to recover all off-screen windows" -ForegroundColor Cyan
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment