Skip to content

Commit c710ed1

Browse files
committed
Fix capture blacklist handling so configured apps aren't wrongly ignored and stale blacklist entries recover automatically.
1 parent 419eaff commit c710ed1

3 files changed

Lines changed: 130 additions & 18 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ That shows a useful pattern:
145145

146146
- Capture learns ignored processes automatically and stores them in `WindowLayout\processes_to_ignore.txt`
147147
- Processes without a valid desktop number are blacklisted and omitted from `current_layout.txt`
148+
- `WindowLayout.cmd -CaptureCurrent -IgnoreBlacklist` captures the full visible window list for troubleshooting, including blacklisted processes and windows without a usable desktop number
148149
- Capture still writes integer percentages, but manual decimal edits in `window_layouts.txt` are preserved when the file is reformatted
149150

150151
## Restore Notes
@@ -160,9 +161,12 @@ That shows a useful pattern:
160161
WindowLayout.cmd
161162
WindowLayout.cmd -ListLayouts
162163
WindowLayout.cmd -CaptureCurrent
164+
WindowLayout.cmd -CaptureCurrent -IgnoreBlacklist
163165
WindowLayout.cmd -ApplyLayout "3 monitors - developer"
164166
```
165167

168+
When you run `-CaptureCurrent` from the command line, the script automatically picks the best saved monitor setup that matches the currently detected monitors.
169+
166170
## License
167171

168172
MIT. See `LICENSE`.

WindowLayout/WindowLayout.ps1

Lines changed: 122 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
param(
33
[string]$ApplyLayout,
44
[switch]$CaptureCurrent,
5-
[switch]$ListLayouts
5+
[switch]$ListLayouts,
6+
[switch]$IgnoreBlacklist
67
)
78

89
Set-StrictMode -Version 3.0
@@ -363,17 +364,31 @@ function Save-IgnoredProcesses {
363364
function Update-IgnoredProcessesFromObservations {
364365
param(
365366
[Parameter(Mandatory = $true)]$ExistingIgnored,
366-
[Parameter(Mandatory = $true)]$Observations
367+
[Parameter(Mandatory = $true)]$Observations,
368+
[hashtable]$ProtectedProcesses = @{}
367369
)
368370

369371
$updated = @{}
370372
foreach ($key in $ExistingIgnored.Keys) {
371373
$updated[$key] = $true
372374
}
373375

376+
foreach ($key in $ProtectedProcesses.Keys) {
377+
if ($updated.ContainsKey($key)) {
378+
$updated.Remove($key)
379+
}
380+
}
381+
374382
foreach ($entry in $Observations.GetEnumerator()) {
375383
$processName = $entry.Key
376384
$stats = $entry.Value
385+
if ($ProtectedProcesses.ContainsKey($processName)) {
386+
if ($updated.ContainsKey($processName)) {
387+
$updated.Remove($processName)
388+
}
389+
continue
390+
}
391+
377392
if ($stats.RealDesktopCount -gt 0) {
378393
if ($updated.ContainsKey($processName)) {
379394
$updated.Remove($processName)
@@ -410,6 +425,28 @@ function Add-IgnoredProcesses {
410425
return $updated
411426
}
412427

428+
function Get-ProtectedProcessesFromConfig {
429+
param($Config)
430+
431+
$result = @{}
432+
if ($null -eq $Config) {
433+
return $result
434+
}
435+
436+
foreach ($layout in @($Config.layouts)) {
437+
foreach ($window in @($layout.windows)) {
438+
$processName = [string](Get-OptionalPropertyValue -Object $window -Name 'processName' -Default '')
439+
if ([string]::IsNullOrWhiteSpace($processName)) {
440+
continue
441+
}
442+
443+
$result[$processName.Trim().ToLowerInvariant()] = $true
444+
}
445+
}
446+
447+
return $result
448+
}
449+
413450
function Show-PendingMessages {
414451
if ($script:PendingMessages.Count -eq 0) {
415452
return
@@ -1285,6 +1322,16 @@ function Get-MonitorMapping {
12851322
[Parameter(Mandatory = $true)]$ActualMonitors
12861323
)
12871324

1325+
$match = Get-MonitorSetupMatch -MonitorSetup $MonitorSetup -ActualMonitors $ActualMonitors
1326+
return $match.Mapping
1327+
}
1328+
1329+
function Get-MonitorSetupMatch {
1330+
param(
1331+
[Parameter(Mandatory = $true)]$MonitorSetup,
1332+
[Parameter(Mandatory = $true)]$ActualMonitors
1333+
)
1334+
12881335
$expected = @($MonitorSetup.monitors)
12891336
$actual = @($ActualMonitors)
12901337
if ($expected.Count -ne $actual.Count) {
@@ -1323,7 +1370,11 @@ function Get-MonitorMapping {
13231370
$mapping[[string]$expected[$i].role] = $actual[$bestPermutation[$i]]
13241371
}
13251372

1326-
return $mapping
1373+
return [pscustomobject]@{
1374+
MonitorSetup = $MonitorSetup
1375+
Mapping = $mapping
1376+
Cost = $bestCost
1377+
}
13271378
}
13281379

13291380
function Get-WindowProcessName {
@@ -1377,11 +1428,13 @@ function Get-WindowRectObject {
13771428

13781429
function Get-OpenWindows {
13791430
param(
1380-
[bool]$IncludeDesktopId = $true
1431+
[bool]$IncludeDesktopId = $true,
1432+
[bool]$IgnoreBlacklist = $false,
1433+
[hashtable]$ProtectedProcesses = @{}
13811434
)
13821435

13831436
$skipClasses = @('Progman', 'WorkerW', 'Shell_TrayWnd')
1384-
$ignoredProcesses = Get-IgnoredProcesses
1437+
$ignoredProcesses = if ($IgnoreBlacklist) { @{} } else { Get-IgnoredProcesses }
13851438
$observations = @{}
13861439
$desktopState = if ($IncludeDesktopId) { New-VirtualDesktopState } else { $null }
13871440
$handles = [WindowLayoutNative]::GetTopLevelWindows()
@@ -1397,7 +1450,8 @@ function Get-OpenWindows {
13971450
}
13981451

13991452
$processKey = $processName.ToLowerInvariant()
1400-
if ($ignoredProcesses.ContainsKey($processKey)) {
1453+
$isIgnoredProcess = $ignoredProcesses.ContainsKey($processKey) -and -not $ProtectedProcesses.ContainsKey($processKey)
1454+
if (-not $IncludeDesktopId -and $isIgnoredProcess) {
14011455
continue
14021456
}
14031457

@@ -1451,15 +1505,24 @@ function Get-OpenWindows {
14511505
DesktopId = $desktopId
14521506
}
14531507

1454-
$result.Add($window)
1508+
if (-not $isIgnoredProcess) {
1509+
$result.Add($window)
1510+
}
14551511

14561512
}
14571513

1458-
if ($IncludeDesktopId) {
1459-
$ignoredProcesses = Update-IgnoredProcessesFromObservations -ExistingIgnored $ignoredProcesses -Observations $observations
1514+
if ($IncludeDesktopId -and -not $IgnoreBlacklist) {
1515+
$ignoredProcesses = Update-IgnoredProcessesFromObservations -ExistingIgnored $ignoredProcesses -Observations $observations -ProtectedProcesses $ProtectedProcesses
14601516
}
14611517

1462-
return @($result.ToArray() | Where-Object { -not $ignoredProcesses.ContainsKey($_.ProcessName.ToLowerInvariant()) })
1518+
if ($IgnoreBlacklist) {
1519+
return @($result.ToArray())
1520+
}
1521+
1522+
return @($result.ToArray() | Where-Object {
1523+
$processKey = $_.ProcessName.ToLowerInvariant()
1524+
(-not $ignoredProcesses.ContainsKey($processKey)) -or $ProtectedProcesses.ContainsKey($processKey)
1525+
})
14631526
}
14641527

14651528
function Test-TitleMatch {
@@ -1759,15 +1822,47 @@ function Select-MonitorSetupForCapture {
17591822
}
17601823
}
17611824

1825+
function Resolve-CaptureTarget {
1826+
param([Parameter(Mandatory = $true)]$Config)
1827+
1828+
$actualMonitors = @(Get-ActualMonitors)
1829+
if ($Config.monitorSetups.Count -eq 0) {
1830+
return Select-MonitorSetupForCapture -Config $Config
1831+
}
1832+
1833+
$matches = New-Object System.Collections.Generic.List[object]
1834+
foreach ($setup in $Config.monitorSetups) {
1835+
try {
1836+
$match = Get-MonitorSetupMatch -MonitorSetup $setup -ActualMonitors $actualMonitors
1837+
$matches.Add($match)
1838+
}
1839+
catch {
1840+
}
1841+
}
1842+
1843+
if ($matches.Count -eq 0) {
1844+
throw "No saved monitor setup matches the currently detected monitors. Run interactive capture to choose or create a monitor setup."
1845+
}
1846+
1847+
$bestMatch = @($matches | Sort-Object Cost, @{ Expression = { $_.MonitorSetup.name } })[0]
1848+
Add-PendingMessage ("Using monitor setup '{0}' for capture." -f $bestMatch.MonitorSetup.name)
1849+
return [pscustomobject]@{
1850+
setup = $bestMatch.MonitorSetup
1851+
isNew = $false
1852+
}
1853+
}
1854+
17621855
function Capture-CurrentLayout {
17631856
param(
17641857
[Parameter(Mandatory = $true)]$Config,
1765-
[Parameter(Mandatory = $true)]$CaptureTarget
1858+
[Parameter(Mandatory = $true)]$CaptureTarget,
1859+
[bool]$IgnoreBlacklist = $false
17661860
)
17671861

17681862
$actualMonitors = @(Get-ActualMonitors)
17691863
$mapping = Get-MonitorMapping -MonitorSetup $CaptureTarget.setup -ActualMonitors $actualMonitors
1770-
$windows = @(Get-OpenWindows | Where-Object { -not $_.IsMinimized } | Sort-Object ProcessName, Title)
1864+
$protectedProcesses = Get-ProtectedProcessesFromConfig -Config $Config
1865+
$windows = @(Get-OpenWindows -IgnoreBlacklist:$IgnoreBlacklist -ProtectedProcesses $protectedProcesses | Where-Object { -not $_.IsMinimized } | Sort-Object ProcessName, Title)
17711866
$desktopState = New-VirtualDesktopState
17721867

17731868
$capturedRows = foreach ($window in $windows) {
@@ -1796,16 +1891,25 @@ function Capture-CurrentLayout {
17961891

17971892
$processesWithBlankDesktop = @(
17981893
$capturedRows |
1799-
Where-Object { $null -eq $_.desktop } |
1800-
ForEach-Object { $_.processName } |
1894+
Group-Object -Property processName |
1895+
Where-Object {
1896+
$processName = [string]$_.Name
1897+
$processKey = $processName.ToLowerInvariant()
1898+
$hasDesktop = @($_.Group | Where-Object { $null -ne $_.desktop }).Count -gt 0
1899+
(-not $protectedProcesses.ContainsKey($processKey)) -and (-not $hasDesktop)
1900+
} |
1901+
ForEach-Object { $_.Name } |
18011902
Sort-Object -Unique
18021903
)
18031904

1804-
if ($processesWithBlankDesktop.Count -gt 0) {
1905+
if (-not $IgnoreBlacklist -and $processesWithBlankDesktop.Count -gt 0) {
18051906
[void](Add-IgnoredProcesses -ProcessNames $processesWithBlankDesktop)
18061907
$capturedRows = @($capturedRows | Where-Object { $processesWithBlankDesktop -notcontains $_.processName })
18071908
Add-PendingMessage ("Ignored processes with no usable desktop: {0}" -f ($processesWithBlankDesktop -join ', '))
18081909
}
1910+
elseif ($IgnoreBlacklist) {
1911+
Add-PendingMessage 'Capture ignored processes blacklist and kept windows even when their desktop number was unavailable.'
1912+
}
18091913

18101914
$layoutWindows = @()
18111915
$groups = $capturedRows | Group-Object -Property processName
@@ -1898,7 +2002,7 @@ function Show-Menu {
18982002
return
18992003
}
19002004

1901-
$snapshot = Capture-CurrentLayout -Config $Config -CaptureTarget $target
2005+
$snapshot = Capture-CurrentLayout -Config $Config -CaptureTarget $target -IgnoreBlacklist:$IgnoreBlacklist
19022006
Save-CapturedLayout -Config $snapshot
19032007
Write-Host ''
19042008
Write-Host ("Saved current layout to '{0}'." -f $script:CurrentLayoutPath)
@@ -1931,12 +2035,12 @@ if ($ListLayouts) {
19312035

19322036
if ($CaptureCurrent) {
19332037
Show-PendingMessages
1934-
$target = Select-MonitorSetupForCapture -Config $config
2038+
$target = Resolve-CaptureTarget -Config $config
19352039
if ($null -eq $target) {
19362040
exit 0
19372041
}
19382042

1939-
$snapshot = Capture-CurrentLayout -Config $config -CaptureTarget $target
2043+
$snapshot = Capture-CurrentLayout -Config $config -CaptureTarget $target -IgnoreBlacklist:$IgnoreBlacklist
19402044
Save-CapturedLayout -Config $snapshot
19412045
Write-Host "Saved current layout to '$script:CurrentLayoutPath'."
19422046
Write-Host "Copy the monitor setup and layout you want into '$script:ConfigPath', remove the rows you do not need, and see '$(Join-Path $script:RootDirectory 'readme.txt')' for details."

WindowLayout/readme.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ How to use it
3131
5. After capture, open current_layout.txt, copy the monitor setup or layout you want into window_layouts.txt, and remove the rows you do not need.
3232
6. Capture usually includes more windows than you want, including helper windows and some windows that are not obvious at first glance.
3333
7. Processes without a valid desktop number are added to processes_to_ignore.txt instead of being written to current_layout.txt.
34+
8. For troubleshooting, run WindowLayout.cmd -CaptureCurrent -IgnoreBlacklist to capture the full visible window list, including blacklisted processes and windows with no usable desktop number.
3435

3536
Direct shortcut script
3637
- You can also run a layout directly from the command line:
@@ -104,6 +105,7 @@ Capture behavior
104105
- The monitor match is tolerant: it does not require a perfect coordinate or size match.
105106
- Capture records the virtual desktop number for each included window.
106107
- Processes without a valid desktop number are added to processes_to_ignore.txt and left out of current_layout.txt.
108+
- Running WindowLayout.cmd -CaptureCurrent -IgnoreBlacklist bypasses that filter and keeps those rows in current_layout.txt with a blank desktop column.
107109
- For multi-instance applications, capture emits both:
108110
- one cascade row with an empty title
109111
- the individual per-window rows
@@ -130,4 +132,6 @@ Command line examples
130132
- WindowLayout.cmd
131133
- WindowLayout.cmd -ListLayouts
132134
- WindowLayout.cmd -CaptureCurrent
135+
- WindowLayout.cmd -CaptureCurrent -IgnoreBlacklist
133136
- WindowLayout.cmd -ApplyLayout "3 monitors - developer"
137+
- Command-line capture automatically picks the best saved monitor setup that matches the currently detected monitors.

0 commit comments

Comments
 (0)