Skip to content

Commit 711bf6c

Browse files
committed
Improve capture workflow and generated layout file guidance
1 parent c710ed1 commit 711bf6c

3 files changed

Lines changed: 148 additions & 58 deletions

File tree

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ When `WindowLayout\window_layouts.txt` is reformatted, the script also regenerat
4444
- `Portable`: runs directly from a folder such as the desktop
4545
- `Monitor setups`: separate physical monitor arrangements from window layouts
4646
- `Relative monitor matching`: matches the current monitors to a saved setup by relative position, not Windows numbering
47-
- `Capture workflow`: saves the current layout to `current_layout.txt`, which you can trim and copy into `window_layouts.txt`
47+
- `Capture workflow`: saves the current layout to `current_layout.txt` with a timestamped comment header, and you can trim and copy parts into `window_layouts.txt`
4848
- `Readable config`: uses a pipe-delimited text format instead of JSON
4949
- `Regex titles`: supports `regex` title matching, including `|` inside regex patterns
5050
- `Cascade support`: capture emits both a cascade row and individual rows for multi-instance apps
5151
- `Virtual desktops`: captures and restores a manually editable desktop number for each window
5252
- `Editable percentages`: capture writes integer `x`, `y`, `w`, and `h` values, but you can manually change them to decimals for finer positioning
53-
- `Ignore list`: processes without a valid desktop number are written to `processes_to_ignore.txt` and excluded from future captures and restores
53+
- `Ignore list`: processes whose captured windows never resolve to a valid desktop number are written to `processes_to_ignore.txt` and excluded from future captures and restores
5454

5555
## Folder Layout
5656

@@ -63,11 +63,11 @@ WindowLayout/
6363
readme.txt
6464
```
6565

66-
`window_layouts.txt` is created on first run.
66+
`window_layouts.txt` is created on first run with a comment header and a blank line before the content.
6767

6868
Per-layout `.cmd` shortcut scripts inside `WindowLayout\` are generated from `window_layouts.txt` when it is reformatted.
6969

70-
`current_layout.txt` is generated only when you capture a layout.
70+
`current_layout.txt` is generated only when you capture a layout. It includes a timestamped comment header and a blank line before the captured content.
7171

7272
`processes_to_ignore.txt` is updated automatically when capture finds processes without a valid desktop number.
7373

@@ -79,7 +79,9 @@ Per-layout `.cmd` shortcut scripts inside `WindowLayout\` are generated from `wi
7979
4. Choose a saved layout, or capture the current layout.
8080
5. After capture, copy the parts you want from `current_layout.txt` into `window_layouts.txt`.
8181

82-
Capture usually sees more windows than you want to keep, including helper windows and some windows that are not obvious at first glance. Processes without a valid desktop number are not written to `current_layout.txt`; instead, their process names are added to `WindowLayout\processes_to_ignore.txt`.
82+
When you create a new monitor setup, capture first lists the detected monitors, then asks you to name each monitor, then asks for the monitor configuration name.
83+
84+
Capture usually sees more windows than you want to keep, including helper windows and some windows that are not obvious at first glance. Individual windows without a valid desktop number are not written to `current_layout.txt`; if all captured windows for a process lack a valid desktop number, that process name is added to `WindowLayout\processes_to_ignore.txt`.
8385

8486
## Configuration Model
8587

@@ -144,7 +146,8 @@ That shows a useful pattern:
144146
## Capture Notes
145147

146148
- Capture learns ignored processes automatically and stores them in `WindowLayout\processes_to_ignore.txt`
147-
- Processes without a valid desktop number are blacklisted and omitted from `current_layout.txt`
149+
- Individual windows without a valid desktop number are omitted from `current_layout.txt`
150+
- If all captured windows for a process have no valid desktop number, that process is blacklisted
148151
- `WindowLayout.cmd -CaptureCurrent -IgnoreBlacklist` captures the full visible window list for troubleshooting, including blacklisted processes and windows without a usable desktop number
149152
- Capture still writes integer percentages, but manual decimal edits in `window_layouts.txt` are preserved when the file is reformatted
150153

WindowLayout/WindowLayout.ps1

Lines changed: 124 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,38 @@ function New-DefaultConfig {
320320
}
321321
}
322322

323+
function Get-ConfigFileHeader {
324+
return @(
325+
'# These layouts are used by the script.',
326+
'# Edit this file to control which window layouts can be applied.',
327+
'',
328+
''
329+
) -join [Environment]::NewLine
330+
}
331+
332+
function Get-CapturedLayoutHeader {
333+
$capturedAt = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
334+
return @(
335+
('# This window layout was captured at {0}.' -f $capturedAt),
336+
'# Copy all or part of this file into window_layouts.txt if you want to save it.',
337+
'# These captured layouts are not used by the script.',
338+
'',
339+
''
340+
) -join [Environment]::NewLine
341+
}
342+
343+
function ConvertTo-ConfigFileText {
344+
param([Parameter(Mandatory = $true)]$Config)
345+
346+
return (Get-ConfigFileHeader) + (ConvertTo-ConfigText -Config $Config)
347+
}
348+
349+
function ConvertTo-CapturedLayoutFileText {
350+
param([Parameter(Mandatory = $true)]$Config)
351+
352+
return (Get-CapturedLayoutHeader) + (ConvertTo-ConfigText -Config $Config)
353+
}
354+
323355
function Add-PendingMessage {
324356
param([Parameter(Mandatory = $true)][string]$Message)
325357

@@ -470,7 +502,7 @@ function Show-MenuHeader {
470502

471503
function Ensure-ConfigFile {
472504
if (-not (Test-Path -LiteralPath $script:ConfigPath)) {
473-
[System.IO.File]::WriteAllText($script:ConfigPath, '', (New-Object System.Text.UTF8Encoding($true)))
505+
[System.IO.File]::WriteAllText($script:ConfigPath, (Get-ConfigFileHeader), (New-Object System.Text.UTF8Encoding($true)))
474506
}
475507
}
476508

@@ -1065,7 +1097,7 @@ function Get-Config {
10651097
$raw = Get-Content -LiteralPath $script:ConfigPath -Raw -Encoding UTF8
10661098
$config = ConvertTo-ConfigFromText -Text $raw
10671099
Validate-Config -Config $config
1068-
$wasReformatted = Write-FormattedConfigIfNeeded -Path $script:ConfigPath -FormattedText (ConvertTo-ConfigText -Config $config)
1100+
$wasReformatted = Write-FormattedConfigIfNeeded -Path $script:ConfigPath -FormattedText (ConvertTo-ConfigFileText -Config $config)
10691101
if ($wasReformatted) {
10701102
[void](Sync-LayoutShortcutScripts -Config $config)
10711103
Add-PendingMessage -Message "Reformatted and saved '$script:ConfigPath'."
@@ -1722,35 +1754,78 @@ function Get-SizePercent {
17221754
return [int]$percent
17231755
}
17241756

1725-
function New-RoleFromCoordinates {
1726-
param(
1727-
[int]$X,
1728-
[int]$Y
1729-
)
1757+
function Get-MonitorLetterLabel {
1758+
param([Parameter(Mandatory = $true)][int]$Index)
17301759

1731-
return "x$X`_y$Y"
1760+
if ($Index -lt 1) {
1761+
throw 'Monitor index must be at least 1.'
1762+
}
1763+
1764+
$value = $Index
1765+
$label = ''
1766+
while ($value -gt 0) {
1767+
$remainder = ($value - 1) % 26
1768+
$label = ([char]([int][char]'A' + $remainder)) + $label
1769+
$value = [int](($value - 1) / 26)
1770+
}
1771+
1772+
return $label
17321773
}
17331774

1734-
function New-MonitorSetupFromCurrentMonitors {
1735-
param(
1736-
[Parameter(Mandatory = $true)][string]$Name,
1737-
[Parameter(Mandatory = $true)]$ActualMonitors
1738-
)
1775+
function Read-RequiredText {
1776+
param([Parameter(Mandatory = $true)][string]$Prompt)
17391777

1740-
$monitors = foreach ($monitor in $ActualMonitors) {
1741-
[pscustomobject]@{
1742-
role = New-RoleFromCoordinates -X $monitor.X -Y $monitor.Y
1778+
while ($true) {
1779+
Write-Host ''
1780+
$value = Read-Host $Prompt
1781+
Write-Host ''
1782+
if (-not [string]::IsNullOrWhiteSpace($value)) {
1783+
return $value.Trim()
1784+
}
1785+
1786+
Write-Host 'Please enter a value.'
1787+
}
1788+
}
1789+
1790+
function New-MonitorSetupInteractively {
1791+
param([Parameter(Mandatory = $true)]$ActualMonitors)
1792+
1793+
Write-Host ('Found {0} monitors:' -f $ActualMonitors.Count)
1794+
for ($i = 0; $i -lt $ActualMonitors.Count; $i++) {
1795+
$monitor = $ActualMonitors[$i]
1796+
$label = Get-MonitorLetterLabel -Index ($i + 1)
1797+
1798+
Write-Host ('Monitor {0}: {1} x {2} at {3},{4}' -f $label, $monitor.Width, $monitor.Height, $monitor.X, $monitor.Y)
1799+
}
1800+
1801+
$monitors = @()
1802+
for ($i = 0; $i -lt $ActualMonitors.Count; $i++) {
1803+
$monitor = $ActualMonitors[$i]
1804+
$label = Get-MonitorLetterLabel -Index ($i + 1)
1805+
$role = Read-RequiredText -Prompt ('Please enter name for monitor {0} (example "Main", "Left", "Wide screen", etc.)' -f $label)
1806+
$monitors += [pscustomobject]@{
1807+
role = $role
17431808
x = $monitor.X
17441809
y = $monitor.Y
17451810
}
17461811
}
17471812

1813+
$name = Read-RequiredText -Prompt 'Please enter name for monitor configuration (example "Main", "Office", etc.)'
1814+
17481815
return [pscustomobject]@{
1749-
name = $Name
1816+
name = $name
17501817
monitors = @($monitors)
17511818
}
17521819
}
17531820

1821+
function New-MonitorSetupFromCurrentMonitors {
1822+
param(
1823+
[Parameter(Mandatory = $true)]$ActualMonitors
1824+
)
1825+
1826+
return New-MonitorSetupInteractively -ActualMonitors $ActualMonitors
1827+
}
1828+
17541829
function Read-ChoiceNumber {
17551830
param(
17561831
[Parameter(Mandatory = $true)][string]$Prompt,
@@ -1761,7 +1836,9 @@ function Read-ChoiceNumber {
17611836
)
17621837

17631838
while ($true) {
1839+
Write-Host ''
17641840
$choice = Read-Host $Prompt
1841+
Write-Host ''
17651842
if ($AllowBlank -and [string]::IsNullOrWhiteSpace($choice)) {
17661843
return $BlankValue
17671844
}
@@ -1780,13 +1857,8 @@ function Select-MonitorSetupForCapture {
17801857

17811858
$actualMonitors = @(Get-ActualMonitors)
17821859
if ($Config.monitorSetups.Count -eq 0) {
1783-
$name = Read-Host "No monitor setups exist yet. Enter a name for the new monitor setup"
1784-
if ([string]::IsNullOrWhiteSpace($name)) {
1785-
$name = 'Current setup'
1786-
}
1787-
17881860
return [pscustomobject]@{
1789-
setup = New-MonitorSetupFromCurrentMonitors -Name $name -ActualMonitors $actualMonitors
1861+
setup = New-MonitorSetupFromCurrentMonitors -ActualMonitors $actualMonitors
17901862
isNew = $true
17911863
}
17921864
}
@@ -1797,21 +1869,16 @@ function Select-MonitorSetupForCapture {
17971869
Write-Host ("{0}. {1}" -f $index, $setup.name)
17981870
$index++
17991871
}
1800-
Write-Host ("{0}. create new monitor setup" -f $index)
1872+
Write-Host ("{0}. Create new monitor setup" -f $index)
18011873

18021874
$selection = Read-ChoiceNumber -Prompt 'Choose an option' -Minimum 1 -Maximum $index -AllowBlank -BlankValue 0
18031875
if ($selection -eq 0) {
18041876
return $null
18051877
}
18061878

18071879
if ($selection -eq $index) {
1808-
$name = Read-Host 'Enter the new monitor setup name'
1809-
if ([string]::IsNullOrWhiteSpace($name)) {
1810-
$name = 'Current setup'
1811-
}
1812-
18131880
return [pscustomobject]@{
1814-
setup = New-MonitorSetupFromCurrentMonitors -Name $name -ActualMonitors $actualMonitors
1881+
setup = New-MonitorSetupFromCurrentMonitors -ActualMonitors $actualMonitors
18151882
isNew = $true
18161883
}
18171884
}
@@ -1889,23 +1956,40 @@ function Capture-CurrentLayout {
18891956
}
18901957
}
18911958

1959+
$rowsWithBlankDesktop = @(
1960+
$capturedRows | Where-Object {
1961+
$processKey = $_.processName.ToLowerInvariant()
1962+
(-not $protectedProcesses.ContainsKey($processKey)) -and $null -eq $_.desktop
1963+
}
1964+
)
1965+
18921966
$processesWithBlankDesktop = @(
1893-
$capturedRows |
1967+
$rowsWithBlankDesktop |
18941968
Group-Object -Property processName |
18951969
Where-Object {
18961970
$processName = [string]$_.Name
18971971
$processKey = $processName.ToLowerInvariant()
1898-
$hasDesktop = @($_.Group | Where-Object { $null -ne $_.desktop }).Count -gt 0
1972+
$hasDesktop = @($capturedRows | Where-Object {
1973+
$_.processName -eq $processName -and $null -ne $_.desktop
1974+
}).Count -gt 0
18991975
(-not $protectedProcesses.ContainsKey($processKey)) -and (-not $hasDesktop)
19001976
} |
19011977
ForEach-Object { $_.Name } |
19021978
Sort-Object -Unique
19031979
)
19041980

1905-
if (-not $IgnoreBlacklist -and $processesWithBlankDesktop.Count -gt 0) {
1906-
[void](Add-IgnoredProcesses -ProcessNames $processesWithBlankDesktop)
1907-
$capturedRows = @($capturedRows | Where-Object { $processesWithBlankDesktop -notcontains $_.processName })
1908-
Add-PendingMessage ("Ignored processes with no usable desktop: {0}" -f ($processesWithBlankDesktop -join ', '))
1981+
if (-not $IgnoreBlacklist -and $rowsWithBlankDesktop.Count -gt 0) {
1982+
if ($processesWithBlankDesktop.Count -gt 0) {
1983+
[void](Add-IgnoredProcesses -ProcessNames $processesWithBlankDesktop)
1984+
Add-PendingMessage ("Ignored processes with no usable desktop: {0}" -f ($processesWithBlankDesktop -join ', '))
1985+
}
1986+
1987+
$capturedRows = @($capturedRows | Where-Object {
1988+
$processKey = $_.processName.ToLowerInvariant()
1989+
$protectedProcesses.ContainsKey($processKey) -or $null -ne $_.desktop
1990+
})
1991+
1992+
Add-PendingMessage ("Skipped {0} captured window(s) with no usable desktop number." -f $rowsWithBlankDesktop.Count)
19091993
}
19101994
elseif ($IgnoreBlacklist) {
19111995
Add-PendingMessage 'Capture ignored processes blacklist and kept windows even when their desktop number was unavailable.'
@@ -1953,7 +2037,7 @@ function Save-CapturedLayout {
19532037
[Parameter(Mandatory = $true)]$Config
19542038
)
19552039

1956-
$text = ConvertTo-ConfigText -Config $Config
2040+
$text = ConvertTo-CapturedLayoutFileText -Config $Config
19572041
Set-Content -LiteralPath $script:CurrentLayoutPath -Value $text -Encoding UTF8
19582042
}
19592043

@@ -1971,15 +2055,14 @@ function Show-Menu {
19712055
foreach ($layout in $Config.layouts) {
19722056
$label = "$($layout.monitorSetup) - $($layout.name)"
19732057
$options += [pscustomobject]@{ Number = $index; Action = 'apply'; Layout = $layout; Label = $label }
1974-
Write-Host ("{0}. set window layout '{1}'" -f $index, $label)
2058+
Write-Host ("{0}. Set window layout '{1}'" -f $index, $label)
19752059
$index++
19762060
}
19772061

19782062
$captureNumber = $index
19792063
$options += [pscustomobject]@{ Number = $captureNumber; Action = 'capture' }
1980-
Write-Host ("{0}. get current window layout" -f $captureNumber)
1981-
Write-Host '0. exit'
1982-
Write-Host ''
2064+
Write-Host ("{0}. Get current window layout" -f $captureNumber)
2065+
Write-Host '0. Exit'
19832066

19842067
$choice = Read-ChoiceNumber -Prompt 'Choose an option' -Minimum 0 -Maximum $captureNumber -AllowBlank -BlankValue 0
19852068
if ($choice -eq 0) {

WindowLayout/readme.txt

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ Why this is useful
1111
Files
1212
- WindowLayout.cmd must stay next to the WindowLayout folder.
1313
- WindowLayout\WindowLayout.ps1 is the main script.
14-
- WindowLayout\window_layouts.txt is the editable layouts file. It is created automatically on first run.
14+
- WindowLayout\window_layouts.txt is the editable layouts file. It is created automatically on first run with a comment header and a blank line before the content.
1515
- WindowLayout\WindowLayout - <monitor setup> - <layout>.cmd files are generated from window_layouts.txt when that file is reformatted.
16-
- WindowLayout\current_layout.txt is generated only when you capture the current layout.
16+
- WindowLayout\current_layout.txt is generated only when you capture the current layout. It includes a comment header with the capture date and time, followed by a blank line.
1717
- WindowLayout\processes_to_ignore.txt is maintained automatically for processes without a valid desktop number.
1818

1919
Setup examples:
@@ -27,11 +27,14 @@ How to use it
2727
1. Double-click WindowLayout.cmd.
2828
2. Choose a layout from the menu in the form "monitor setup - layout".
2929
3. Or choose to capture the current layout.
30-
4. During capture, choose which monitor setup to map the current monitors to.
31-
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.
32-
6. Capture usually includes more windows than you want, including helper windows and some windows that are not obvious at first glance.
33-
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.
30+
4. During capture, choose which monitor setup to map the current monitors to, or create a new one.
31+
5. When creating a new monitor setup, capture first lists all detected monitors, then asks you to name each monitor, then asks for the monitor configuration name.
32+
6. 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.
33+
7. current_layout.txt is only a captured snapshot and is not used by the script unless you copy parts of it into window_layouts.txt.
34+
8. Capture usually includes more windows than you want, including helper windows and some windows that are not obvious at first glance.
35+
9. Individual windows without a valid desktop number are skipped in current_layout.txt.
36+
10. If all captured windows for a process have no valid desktop number, that process is added to processes_to_ignore.txt.
37+
11. For troubleshooting, run WindowLayout.cmd -CaptureCurrent -IgnoreBlacklist to capture the full visible window list, including blacklisted processes and windows with no usable desktop number.
3538

3639
Direct shortcut script
3740
- You can also run a layout directly from the command line:
@@ -100,11 +103,12 @@ Monitor setups
100103
- The script matches the currently connected monitors to the selected monitor setup by relative monitor positions, not by Windows monitor numbering.
101104

102105
Capture behavior
103-
- If the config has no monitor setups yet, capture asks for a name and creates the first monitor setup from the current monitors.
104-
- If monitor setups already exist, capture asks which setup to use, or lets you create a new one.
106+
- If the config has no monitor setups yet, capture lists the detected monitors, asks you to name each monitor, then asks for the monitor configuration name.
107+
- If monitor setups already exist, capture asks which setup to use, or lets you create a new one with the same interactive naming flow.
105108
- The monitor match is tolerant: it does not require a perfect coordinate or size match.
106109
- Capture records the virtual desktop number for each included window.
107-
- Processes without a valid desktop number are added to processes_to_ignore.txt and left out of current_layout.txt.
110+
- Individual windows without a valid desktop number are left out of current_layout.txt.
111+
- If all captured windows for a process have no valid desktop number, that process is added to processes_to_ignore.txt.
108112
- Running WindowLayout.cmd -CaptureCurrent -IgnoreBlacklist bypasses that filter and keeps those rows in current_layout.txt with a blank desktop column.
109113
- For multi-instance applications, capture emits both:
110114
- one cascade row with an empty title
@@ -126,7 +130,7 @@ Menu behavior
126130

127131
Restart from scratch
128132
- Delete WindowLayout\window_layouts.txt and WindowLayout\current_layout.txt.
129-
- On the next run, the script recreates a fresh empty window_layouts.txt.
133+
- On the next run, the script recreates window_layouts.txt with its comment header and blank line.
130134

131135
Command line examples
132136
- WindowLayout.cmd

0 commit comments

Comments
 (0)