# 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 '([^<]*)') { 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
}