# Render the dashboard to a PNG and make it the actual Windows wallpaper. # # Same trick as the camera wallpaper it replaces: because this sets the *real* # wallpaper it needs no window, no re-parenting into WorkerW and no supervision. # Desktop icons sit on top of it correctly, and it survives hibernate, lock and # reboot for free. # # The loop wakes every minute to take a CPU sample (the throttle chart needs # per-minute resolution) and re-renders every -Every seconds (5 min by default). # A render is one short headless Chrome pass, ~1.5-3s, so the steady-state cost # is about 1% of one core. # # pwsh scripts\dashboard-wallpaper.ps1 -Once # pwsh scripts\dashboard-wallpaper.ps1 -Once -Icons # ...and seat the icons # pwsh scripts\dashboard-wallpaper.ps1 -Detach # every 5 min, in the background # pwsh scripts\dashboard-wallpaper.ps1 -Status # pwsh scripts\dashboard-wallpaper.ps1 -Stop # pwsh scripts\dashboard-wallpaper.ps1 -Restore # put the old wallpaper back param( [switch]$Once, [int]$Every = 300, # seconds between renders [int]$SampleEvery = 60, # seconds between CPU samples [switch]$Detach, [switch]$Stop, [switch]$Restore, [switch]$Status, [switch]$Icons, # re-seat desktop icons on every render [switch]$Verify # also report whether any panel overflowed ) $ErrorActionPreference = 'Stop' $root = Split-Path $PSScriptRoot -Parent $dash = Join-Path $root 'dashboard' $page = Join-Path $dash 'dashboard.html' $outDir = Join-Path $root 'wallpaper-out' $profile = Join-Path $env:TEMP 'dashboard-wallpaper-chrome' $pidFile = Join-Path $root '.dashboard-wallpaper.pid' $logFile = Join-Path $root '.dashboard-wallpaper.log' $origFile = Join-Path $root 'baseline\original-wallpaper.txt' New-Item -ItemType Directory -Force -Path $outDir, (Join-Path $root 'baseline') | Out-Null Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public static class Wall { [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool SystemParametersInfo(uint action, uint param, string value, uint winIni); public static bool Set(string path) { return SystemParametersInfo(0x0014, 0, path, 0x01 | 0x02); // SPI_SETDESKWALLPAPER } } '@ function Log($m) { $line = "{0} {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $m $line | Out-File $logFile -Append -Encoding utf8 Write-Host $line } function Get-Chrome { foreach ($p in @( "$env:ProgramFiles\Google\Chrome\Application\chrome.exe", "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe", "$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe" )) { if (Test-Path $p) { return $p } } throw 'no Chrome or Edge found to render with' } # Run a native exe and capture output as text. With $ErrorActionPreference='Stop' # a bare `native.exe 2>&1` promotes any stderr line to a TERMINATING error, and # Chrome chatters on stderr even on a clean run. function Invoke-Native($exe, [string[]]$argList) { $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { return (& $exe @argList 2>&1 | Out-String) } finally { $ErrorActionPreference = $prev } } $chromeCommon = @( '--headless=new', '--disable-gpu', '--hide-scrollbars', '--force-device-scale-factor=1', '--window-size=3000,2000', '--virtual-time-budget=5000', '--no-first-run', '--no-default-browser-check', '--disable-extensions', '--disable-background-networking', "--user-data-dir=$profile" ) function Invoke-Collect { $out = Invoke-Native 'node' @((Join-Path $dash 'collect.mjs')) $line = ($out -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1).Trim() if (-not (Test-Path (Join-Path $dash 'data.js'))) { throw "collect.mjs produced no data.js: $line" } return $line } # The page measures its own panels and reports the result in ; a render # that silently clipped a panel is worse than one that says so. function Test-Fit { $dom = Invoke-Native (Get-Chrome) ($chromeCommon + @('--dump-dom', "file:///$($page -replace '\\','/')")) if ($dom -match '<title>([^<]*)') { return $Matches[1] } return 'fit unknown' } function Update-Wallpaper { # Alternate between two files: Windows caches the wallpaper by path, so # rewriting the same filename often redisplays the stale image. $a = Join-Path $outDir 'dash-a.png'; $b = Join-Path $outDir 'dash-b.png' $slot = if (-not (Test-Path $a)) { $a } elseif (-not (Test-Path $b)) { $b } elseif ((Get-Item $a).LastWriteTime -gt (Get-Item $b).LastWriteTime) { $b } else { $a } if (Test-Path $slot) { Remove-Item $slot -Force -EA SilentlyContinue } $t0 = Get-Date $out = Invoke-Native (Get-Chrome) ($chromeCommon + @( '--default-background-color=0d0d0dff', "--screenshot=$slot", "file:///$($page -replace '\\','/')" )) $ms = [int]((Get-Date) - $t0).TotalMilliseconds # Judge by the artifact, not the exit code. if (-not (Test-Path $slot) -or (Get-Item $slot).Length -lt 20000) { $why = ($out -split "`n" | Where-Object { $_.Trim() } | Select-Object -First 2) -join ' / ' throw "render produced no usable image: $why" } # Windows re-encodes the wallpaper to JPEG internally. At the default quality # that turns 15px mono labels to mush, so ask for the top quality first. Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name JPEGImportQuality -Value 100 -Type DWord -Force Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name WallpaperStyle -Value '10' # Fill Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name TileWallpaper -Value '0' if (-not [Wall]::Set($slot)) { throw 'SystemParametersInfo failed' } return @{ File = (Split-Path $slot -Leaf); Ms = $ms; Kb = [int]((Get-Item $slot).Length / 1KB) } } function Invoke-Sample { try { & powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File (Join-Path $dash 'sample.ps1') | Out-Null } catch { Log "cpu sample failed: $($_.Exception.Message)" } } function Invoke-Refresh { $summary = Invoke-Collect $r = Update-Wallpaper $msg = "rendered $($r.File) in $($r.Ms)ms ($($r.Kb) KB) - $summary" if ($Icons) { try { $ic = & powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass ` -File (Join-Path $PSScriptRoot 'icon-layout.ps1') -Quiet 2>&1 | Select-Object -Last 1 $msg += " | $($ic -replace '\s+', ' ')" } catch { $msg += " | icon placement failed: $($_.Exception.Message)" } } Log $msg if ($Verify) { Log ("fit: " + (Test-Fit)) } } function Save-Original { if (Test-Path $origFile) { return } $cur = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name WallPaper -EA SilentlyContinue).WallPaper if ($cur) { $cur | Set-Content $origFile; Log "saved original wallpaper: $cur" } } # ---------------------------------------------------------------- control paths if ($Status) { $running = (Test-Path $pidFile) -and (Get-Process -Id (Get-Content $pidFile) -EA SilentlyContinue) "loop: $(if ($running) { 'running (pid ' + (Get-Content $pidFile) + ')' } else { 'not running' })" $newest = Get-ChildItem $outDir -Filter 'dash-*.png' -EA SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($newest) { $age = (Get-Date) - $newest.LastWriteTime $stale = $age.TotalSeconds -gt ($Every * 2.5) "last render: {0} ({1:n0} min ago){2}" -f $newest.LastWriteTime.ToString('HH:mm:ss'), $age.TotalMinutes, $(if ($stale) { ' <-- STALE' } else { '' }) } else { "last render: never" } $cpuFile = Join-Path $dash 'state\cpu.jsonl' if (Test-Path $cpuFile) { $n = @(Get-Content $cpuFile).Count $age = ((Get-Date) - (Get-Item $cpuFile).LastWriteTime).TotalMinutes "cpu samples: {0} in buffer, newest {1:n0} min ago" -f $n, $age } else { "cpu samples: none yet" } "wallpaper: $((Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name WallPaper).WallPaper)" "original: $(if (Test-Path $origFile) { Get-Content $origFile } else { '(not saved)' })" if (Test-Path $logFile) { '--- last log ---'; Get-Content $logFile -Tail 6 } return } if ($Stop) { if (Test-Path $pidFile) { Stop-Process -Id (Get-Content $pidFile) -Force -EA SilentlyContinue Remove-Item $pidFile -Force -EA SilentlyContinue } 'dashboard loop stopped (wallpaper left as-is; use -Restore to put the old one back)' return } if ($Restore) { if (Test-Path $pidFile) { Stop-Process -Id (Get-Content $pidFile) -Force -EA SilentlyContinue Remove-Item $pidFile -Force -EA SilentlyContinue } if (-not (Test-Path $origFile)) { throw 'no saved original wallpaper' } $orig = Get-Content $origFile [Wall]::Set($orig) | Out-Null "restored: $orig" return } Save-Original if ($Once) { Invoke-Sample; Invoke-Refresh; return } if ($Detach) { if (Test-Path $pidFile) { Stop-Process -Id (Get-Content $pidFile) -Force -EA SilentlyContinue } # Start-Process joins -ArgumentList with spaces and quotes nothing, so build # the command line explicitly and quote the path ourselves. $a = @( '-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', ('"{0}"' -f $PSCommandPath), '-Every', $Every, '-SampleEvery', $SampleEvery ) if ($Icons) { $a += '-Icons' } if ($Verify) { $a += '-Verify' } $p = Start-Process powershell -ArgumentList ($a -join ' ') -WindowStyle Hidden -PassThru $p.Id | Set-Content $pidFile "dashboard wallpaper running (pid $($p.Id)), rendering every ${Every}s, sampling every ${SampleEvery}s" " status: pwsh scripts\dashboard-wallpaper.ps1 -Status" " stop: pwsh scripts\dashboard-wallpaper.ps1 -Stop" return } # ---------------------------------------------------------------- loop $PID | Set-Content $pidFile Log "loop up (pid $PID) - render every ${Every}s, sample every ${SampleEvery}s, icons=$($Icons.IsPresent)" $fails = 0 $lastRender = [datetime]::MinValue while ($true) { Invoke-Sample if (((Get-Date) - $lastRender).TotalSeconds -ge $Every) { try { Invoke-Refresh if ($fails) { Log "recovered after $fails failed render(s)" } $fails = 0 $lastRender = Get-Date } catch { $fails++ Log "render error ($fails in a row): $($_.Exception.Message)" # Back off so a persistent failure does not retry every minute, but # keep sampling - the CPU chart stays useful even when rendering is broken. if ($fails -ge 3) { $lastRender = (Get-Date).AddSeconds(-$Every + 300) } else { $lastRender = (Get-Date).AddSeconds(-$Every + 60) } } } Start-Sleep -Seconds $SampleEvery }