PowerBGInfo
PowerBGInfo is an Evotec PowerShell module that puts machine details, charts, tables and diagrams on Windows desktop and lock-screen wallpapers. You bring the data; ChartForgeX draws the whole image, from the generated backdrop to the last chart label.
- Scenarios
- 12
- Theme presets
- 6
- Cmdlets
- 32
Install-Module PowerBGInfo 
Twelve wallpapers, each with one job
Each script answers one question for the person in front of the screen, and each renders in light and dark at any resolution. A theme preset gives each its look and regions place the panels. Every script starts with a data block of fixed samples, so anyone can reproduce the renders, and a comment beside each value names the command that supplies it on a real machine. Three are shown in full here, with the New-BGInfo call that draws each one and the data block it reads.
Hyper-V host
Room for another VM, and which VMs use the host.
New-BGInfo -Theme $look -Target File {
New-BGInfoBackground -Width $Width -Height $Height -Pattern Grid `
-PatternSpacing ([int](56 * $scale))
# A wider right column for the tables; the treemap may fill the center.
New-BGInfoLayout -ColumnWidth ([int](520 * $scale)) `
-MaximumPanelHeight ([int](640 * $scale))
New-BGInfoLabel -Name 'HYPER-V HOST · PRODUCTION CLUSTER' -Accent `
-FontSize ([int](15 * $scale)) -FontWeight 700
New-BGInfoLabel -Name $hv.Name -FontSize ([int](72 * $scale)) `
-FontWeight 600
New-BGInfoLabel -Name "Room for $free GB of VMs before failover reserve" `
-FontSize ([int](19 * $scale)) -FontWeight 600
New-BGInfoValue -Name 'Cluster' -Value $hv.Cluster
New-BGInfoValue -Name 'Hardware' -Value $hv.Hardware
New-BGInfoValue -Name 'System' -Value $hv.Version
# Top band: four host metrics beside the text. Above 80 % memory or
# 75 % storage the card turns amber; above 90 % it turns red.
$card = @{ Region = 'Top'; Height = [int](170 * $scale) }
New-BGInfoMetric @card -Label 'CPU' -Value $hv.Cpu[-1] -Unit '%' `
-Caption '64 logical cores' -History $hv.Cpu -HistoryMaximum 100 `
-WarningThreshold 70 -CriticalThreshold 85
New-BGInfoMetric @card -Label 'Memory' -Value $memory -Unit '%' `
-Caption "$($hv.MemoryGB) of $($hv.TotalGB) GB assigned" `
-WarningThreshold 80 -CriticalThreshold 90
New-BGInfoMetric @card -Label $hv.Storage.Name -Unit '%' `
-Value $hv.Storage.UsedPercent `
-Caption "$($hv.Storage.FreeTB) TB free" `
-WarningThreshold 75 -CriticalThreshold 90
New-BGInfoMetric @card -Label 'VMs running' -Value $running `
-Caption "$vcpu vCPU on 64 cores"
# Center: where the memory goes, grouped by workload. The color is the
# VM's CPU: amber from 60 %, red from 85 %.
$hv.Vms | New-BGInfoTreemap -Region Center -Title 'Memory by VM' `
-Note 'size: memory · color: CPU' -ValueProperty MemoryGB `
-GroupProperty Workload -ValueSuffix ' GB' -ColorProperty Cpu `
-WarningThreshold 60 -CriticalThreshold 85
# Right column: where else VMs could go, today's live migrations and
# the checkpoints left behind.
New-BGInfoChart -Region Right -Id 'nodes' -Title 'Cluster memory' `
-Kind ProgressBar -Values @($hv.Nodes.Values) `
-Labels @($hv.Nodes.Keys) -Maximum 100 -ValueSuffix ' %' `
-NoProgressHandles -ShowLatestValue:$false -NoHistory `
-WarningThreshold 80 -CriticalThreshold 90 `
-Height ([int](250 * $scale))
$hv.Moves | New-BGInfoTable -Region Right -Title 'Live migrations' `
-Note 'today' -Property Time, VM, Result -StatusColumn Result
New-BGInfoList -Region Right -Title 'Old checkpoints' `
-Note 'older than 7 days' -Item $hv.Checkpoints
} -ConfigurationDirectory $OutputDirectory `
-OutputFileName "hyperv-host-$($Theme.ToLower()).png"
#endregion
#region Data: your logic goes here
function Get-HyperVHostData {
# Get-VM returns these properties (MemoryAssigned, ProcessorCount and
# CPUUsage); the workload tag comes from the VM notes or your CMDB.
# The sample keeps them as CSV to stay short.
$vms = @'
Name,Workload,vCPU,MemoryGB,Cpu
SQL-PROD-01,Databases,16,128,71
SQL-PROD-02,Databases,16,96,34
SQL-RPT-01,Databases,8,48,12
ERP-APP-01,ERP,12,64,38
ERP-APP-02,ERP,12,64,29
ERP-BATCH-01,ERP,8,32,55
VDI-POOL-A,Desktops,48,96,41
VDI-POOL-B,Desktops,24,48,18
WEB-01,Web,4,16,22
WEB-02,Web,4,16,19
WEB-03,Web,4,16,9
FS-01,Infrastructure,4,32,6
MON-01,Infrastructure,4,24,14
DC02,Infrastructure,2,8,3
'@ | ConvertFrom-Csv | ForEach-Object {
[pscustomobject] @{
Name = $_.Name; Workload = $_.Workload; vCPU = [int] $_.vCPU
MemoryGB = [int] $_.MemoryGB; Cpu = [int] $_.Cpu; State = 'Running'
}
}
[pscustomobject] @{
Name = 'HV-AMS-07'
# Get-Cluster and Get-ClusterNode
Cluster = 'HVC-AMS-01 · node 3 of 6'
Hardware = '2 × Xeon Gold 6448Y · 64 cores'
Version = 'Windows Server 2025 Datacenter'
# Get-Counter '\Hyper-V Hypervisor Logical Processor(_Total)\
# % Total Run Time' sampled every 5 minutes
Cpu = @(38, 41, 52, 47, 44, 49, 46)
# (Get-VMHost).MemoryCapacity and the sum of MemoryAssigned
MemoryGB = [int] ($vms | Measure-Object -Property MemoryGB -Sum).Sum
# The cluster keeps one node's worth of memory free for failover.
ReserveGB = 64
TotalGB = 768
# Get-ClusterSharedVolume | Select-Object -ExpandProperty
# SharedVolumeInfo | Select-Object -ExpandProperty Partition
Storage = @{ Name = 'CSV-02'; UsedPercent = 68; FreeTB = 6.1 }
Vms = $vms
# Get-ClusterGroup | Where-Object OwnerNode -NE $env:COMPUTERNAME,
# or the Failover Clustering event log, event 5143 and 1641
Moves = @(
[pscustomobject] @{ Time = '09:12'; VM = 'ERP-BATCH-01'
Move = 'from HV-AMS-04'; Result = 'OK' }
[pscustomobject] @{ Time = '08:47'; VM = 'WEB-03'
Move = 'from HV-AMS-02'; Result = 'OK' }
[pscustomobject] @{ Time = '02:05'; VM = 'VDI-POOL-B'
Move = 'to HV-AMS-05'; Result = 'Failed' }
)
# Memory assigned on every node: Get-ClusterNode | ForEach-Object {
# Get-VM -ComputerName $_.Name | Measure-Object MemoryAssigned }
Nodes = [ordered] @{
'HV-AMS-04' = 71; 'HV-AMS-05' = 64; 'HV-AMS-07' = 82
'HV-AMS-08' = 91; 'HV-AMS-09' = 58; 'HV-AMS-10' = 47
}
# Get-VM | Get-VMSnapshot | Where-Object CreationTime -LT
# (Get-Date).AddDays(-7)
Checkpoints = @(
@{ Text = 'ERP-APP-02 · before CU 14'; Status = 'Warning'
Value = '12 days' }
@{ Text = 'WEB-02 · config test'; Status = 'Warning'
Value = '9 days' }
)
}
}
$fallback = [pscustomobject] @{
Name = $env:COMPUTERNAME; Cluster = 'unknown'; Hardware = 'unknown'
Version = 'unknown'; Cpu = @(0); MemoryGB = 0; TotalGB = 1; ReserveGB = 0
Storage = @{ Name = 'CSV'; UsedPercent = 0; FreeTB = 0 }
Vms = @([pscustomobject] @{ Name = 'unknown'; Workload = ''
vCPU = 0; MemoryGB = 1; Cpu = 0; State = 'Unknown' })
Moves = @(); Checkpoints = @(); Nodes = [ordered] @{ 'unknown' = 0 }
}
$source = @{
Name = 'Hyper-V host'
TimeoutSeconds = 20
ScriptBlock = ${function:Get-HyperVHostData}
Default = $fallback
}
$hv = Invoke-BGInfoDataSource @source
#endregion
Full script 170 lines
<#
Hyper-V host. The question for the administrator at this console: does
this host have room for another VM, and which VMs are using it?
The data block returns plain values; the presentation block only reads
them. Comments name the command or source that supplies each value.
#>
param(
[ValidateSet('Light', 'Dark')]
[string] $Theme = 'Dark',
[int] $Width = 1920,
[int] $Height = 1080,
[string] $OutputDirectory = (Join-Path $PSScriptRoot 'Output')
)
Import-Module (Join-Path $PSScriptRoot '..\PowerBGInfo.psd1') -Force
#region Data: your logic goes here
function Get-HyperVHostData {
# Get-VM returns these properties (MemoryAssigned, ProcessorCount and
# CPUUsage); the workload tag comes from the VM notes or your CMDB.
# The sample keeps them as CSV to stay short.
$vms = @'
Name,Workload,vCPU,MemoryGB,Cpu
SQL-PROD-01,Databases,16,128,71
SQL-PROD-02,Databases,16,96,34
SQL-RPT-01,Databases,8,48,12
ERP-APP-01,ERP,12,64,38
ERP-APP-02,ERP,12,64,29
ERP-BATCH-01,ERP,8,32,55
VDI-POOL-A,Desktops,48,96,41
VDI-POOL-B,Desktops,24,48,18
WEB-01,Web,4,16,22
WEB-02,Web,4,16,19
WEB-03,Web,4,16,9
FS-01,Infrastructure,4,32,6
MON-01,Infrastructure,4,24,14
DC02,Infrastructure,2,8,3
'@ | ConvertFrom-Csv | ForEach-Object {
[pscustomobject] @{
Name = $_.Name; Workload = $_.Workload; vCPU = [int] $_.vCPU
MemoryGB = [int] $_.MemoryGB; Cpu = [int] $_.Cpu; State = 'Running'
}
}
[pscustomobject] @{
Name = 'HV-AMS-07'
# Get-Cluster and Get-ClusterNode
Cluster = 'HVC-AMS-01 · node 3 of 6'
Hardware = '2 × Xeon Gold 6448Y · 64 cores'
Version = 'Windows Server 2025 Datacenter'
# Get-Counter '\Hyper-V Hypervisor Logical Processor(_Total)\
# % Total Run Time' sampled every 5 minutes
Cpu = @(38, 41, 52, 47, 44, 49, 46)
# (Get-VMHost).MemoryCapacity and the sum of MemoryAssigned
MemoryGB = [int] ($vms | Measure-Object -Property MemoryGB -Sum).Sum
# The cluster keeps one node's worth of memory free for failover.
ReserveGB = 64
TotalGB = 768
# Get-ClusterSharedVolume | Select-Object -ExpandProperty
# SharedVolumeInfo | Select-Object -ExpandProperty Partition
Storage = @{ Name = 'CSV-02'; UsedPercent = 68; FreeTB = 6.1 }
Vms = $vms
# Get-ClusterGroup | Where-Object OwnerNode -NE $env:COMPUTERNAME,
# or the Failover Clustering event log, event 5143 and 1641
Moves = @(
[pscustomobject] @{ Time = '09:12'; VM = 'ERP-BATCH-01'
Move = 'from HV-AMS-04'; Result = 'OK' }
[pscustomobject] @{ Time = '08:47'; VM = 'WEB-03'
Move = 'from HV-AMS-02'; Result = 'OK' }
[pscustomobject] @{ Time = '02:05'; VM = 'VDI-POOL-B'
Move = 'to HV-AMS-05'; Result = 'Failed' }
)
# Memory assigned on every node: Get-ClusterNode | ForEach-Object {
# Get-VM -ComputerName $_.Name | Measure-Object MemoryAssigned }
Nodes = [ordered] @{
'HV-AMS-04' = 71; 'HV-AMS-05' = 64; 'HV-AMS-07' = 82
'HV-AMS-08' = 91; 'HV-AMS-09' = 58; 'HV-AMS-10' = 47
}
# Get-VM | Get-VMSnapshot | Where-Object CreationTime -LT
# (Get-Date).AddDays(-7)
Checkpoints = @(
@{ Text = 'ERP-APP-02 · before CU 14'; Status = 'Warning'
Value = '12 days' }
@{ Text = 'WEB-02 · config test'; Status = 'Warning'
Value = '9 days' }
)
}
}
$fallback = [pscustomobject] @{
Name = $env:COMPUTERNAME; Cluster = 'unknown'; Hardware = 'unknown'
Version = 'unknown'; Cpu = @(0); MemoryGB = 0; TotalGB = 1; ReserveGB = 0
Storage = @{ Name = 'CSV'; UsedPercent = 0; FreeTB = 0 }
Vms = @([pscustomobject] @{ Name = 'unknown'; Workload = ''
vCPU = 0; MemoryGB = 1; Cpu = 0; State = 'Unknown' })
Moves = @(); Checkpoints = @(); Nodes = [ordered] @{ 'unknown' = 0 }
}
$source = @{
Name = 'Hyper-V host'
TimeoutSeconds = 20
ScriptBlock = ${function:Get-HyperVHostData}
Default = $fallback
}
$hv = Invoke-BGInfoDataSource @source
#endregion
#region Presentation: reads only $hv
$scale = [Math]::Round($Height / 1080, 2)
$look = New-BGInfoTheme Corporate -Mode $Theme -Scale $scale
$memory = [Math]::Round(100 * $hv.MemoryGB / $hv.TotalGB)
$running = @($hv.Vms | Where-Object State -EQ 'Running').Count
$vcpu = ($hv.Vms | Measure-Object vCPU -Sum).Sum
$free = $hv.TotalGB - $hv.MemoryGB - $hv.ReserveGB
New-BGInfo -Theme $look -Target File {
New-BGInfoBackground -Width $Width -Height $Height -Pattern Grid `
-PatternSpacing ([int](56 * $scale))
# A wider right column for the tables; the treemap may fill the center.
New-BGInfoLayout -ColumnWidth ([int](520 * $scale)) `
-MaximumPanelHeight ([int](640 * $scale))
New-BGInfoLabel -Name 'HYPER-V HOST · PRODUCTION CLUSTER' -Accent `
-FontSize ([int](15 * $scale)) -FontWeight 700
New-BGInfoLabel -Name $hv.Name -FontSize ([int](72 * $scale)) `
-FontWeight 600
New-BGInfoLabel -Name "Room for $free GB of VMs before failover reserve" `
-FontSize ([int](19 * $scale)) -FontWeight 600
New-BGInfoValue -Name 'Cluster' -Value $hv.Cluster
New-BGInfoValue -Name 'Hardware' -Value $hv.Hardware
New-BGInfoValue -Name 'System' -Value $hv.Version
# Top band: four host metrics beside the text. Above 80 % memory or
# 75 % storage the card turns amber; above 90 % it turns red.
$card = @{ Region = 'Top'; Height = [int](170 * $scale) }
New-BGInfoMetric @card -Label 'CPU' -Value $hv.Cpu[-1] -Unit '%' `
-Caption '64 logical cores' -History $hv.Cpu -HistoryMaximum 100 `
-WarningThreshold 70 -CriticalThreshold 85
New-BGInfoMetric @card -Label 'Memory' -Value $memory -Unit '%' `
-Caption "$($hv.MemoryGB) of $($hv.TotalGB) GB assigned" `
-WarningThreshold 80 -CriticalThreshold 90
New-BGInfoMetric @card -Label $hv.Storage.Name -Unit '%' `
-Value $hv.Storage.UsedPercent `
-Caption "$($hv.Storage.FreeTB) TB free" `
-WarningThreshold 75 -CriticalThreshold 90
New-BGInfoMetric @card -Label 'VMs running' -Value $running `
-Caption "$vcpu vCPU on 64 cores"
# Center: where the memory goes, grouped by workload. The color is the
# VM's CPU: amber from 60 %, red from 85 %.
$hv.Vms | New-BGInfoTreemap -Region Center -Title 'Memory by VM' `
-Note 'size: memory · color: CPU' -ValueProperty MemoryGB `
-GroupProperty Workload -ValueSuffix ' GB' -ColorProperty Cpu `
-WarningThreshold 60 -CriticalThreshold 85
# Right column: where else VMs could go, today's live migrations and
# the checkpoints left behind.
New-BGInfoChart -Region Right -Id 'nodes' -Title 'Cluster memory' `
-Kind ProgressBar -Values @($hv.Nodes.Values) `
-Labels @($hv.Nodes.Keys) -Maximum 100 -ValueSuffix ' %' `
-NoProgressHandles -ShowLatestValue:$false -NoHistory `
-WarningThreshold 80 -CriticalThreshold 90 `
-Height ([int](250 * $scale))
$hv.Moves | New-BGInfoTable -Region Right -Title 'Live migrations' `
-Note 'today' -Property Time, VM, Result -StatusColumn Result
New-BGInfoList -Region Right -Title 'Old checkpoints' `
-Note 'older than 7 days' -Item $hv.Checkpoints
} -ConfigurationDirectory $OutputDirectory `
-OutputFileName "hyperv-host-$($Theme.ToLower()).png"
#endregion
Help desk
How to get help, plus the details support asks for.
New-BGInfo -Theme $look -TextPosition MiddleLeft -Target File {
New-BGInfoBackground -Width $Width -Height $Height
# The tile column sits vertically centered beside the centered text.
New-BGInfoLayout -ColumnAlignment Center
New-BGInfoLabel -Name 'IT SERVICE DESK' -Accent `
-FontSize ([int](16 * $scale)) -FontWeight 700
New-BGInfoLabel -Name 'Need help?' -FontSize ([int](88 * $scale)) `
-FontWeight 600
New-BGInfoLabel -Name 'Call 555-0100, extension 4357' -Accent `
-FontSize ([int](40 * $scale)) -FontWeight 600
New-BGInfoLabel -Name 'Have the asset tag on the right ready.' -Muted `
-FontSize ([int](22 * $scale))
New-BGInfoValue -Name 'Portal' -Value 'help.contoso.com'
New-BGInfoValue -Name 'Teams' -Value '#it-help'
New-BGInfoValue -Name 'Walk-in' -Value 'Building B, room 012'
New-BGInfoValue -Name 'Hours' -Value 'Monday–Friday, 07:00–19:00'
New-BGInfoLabel -Name ' ' -FontSize ([int](12 * $scale))
New-BGInfoLabel -Name 'SERVICE STATUS' -Accent `
-FontSize ([int](16 * $scale)) -FontWeight 700
foreach ($service in $facts.Services) {
New-BGInfoLabel -Name "● $($service.Text)" -Status $service.Status `
-FontSize ([int](19 * $scale)) -FontWeight 600
}
New-BGInfoVisualCanvas -Region Right -Tile $tiles
} -ConfigurationDirectory $OutputDirectory `
-OutputFileName "helpdesk-$($Theme.ToLower()).png"
#endregion
#region Data: your logic goes here
function Get-HelpDeskFacts {
[pscustomobject] @{
# (Get-CimInstance Win32_SystemEnclosure).SMBIOSAssetTag
AssetTag = 'CT-04217'
ComputerName = 'FRONTDESK-07'
Location = 'Reception, B1'
# (Get-NetIPConfiguration |
# Where-Object IPv4DefaultGateway).IPv4Address.IPAddress
IPAddress = '10.20.4.37'
Network = 'Wired · VLAN 120'
SignedInAs = 'Reception (shared)'
# (Get-CimInstance Win32_OperatingSystem).Caption and .BuildNumber
Windows = '11 Enterprise 24H2'
Build = '26100'
# Your monitoring or status-page API
Services = @(
@{ Text = 'Email, Teams, Wi-Fi and VPN are working'; Status = 'Ok' }
@{
Text = 'Floor 2 printers are offline until 14:00'
Status = 'Warning'
}
)
}
}
$fallback = [pscustomobject] @{
AssetTag = 'unknown'; ComputerName = $env:COMPUTERNAME
Location = 'unknown'; IPAddress = 'unknown'; Network = ''
SignedInAs = 'unknown'; Windows = 'unknown'; Build = ''
Services = @(@{ Text = 'Service status is unavailable'; Status = 'Info' })
}
$source = @{
Name = 'Help-desk facts'
TimeoutSeconds = 10
ScriptBlock = ${function:Get-HelpDeskFacts}
Default = $fallback
}
$facts = Invoke-BGInfoDataSource @source
#endregion
Full script 107 lines
<#
Help-desk wallpaper for shared or front-desk PCs. It tells people how
to get help and shows the details the service desk asks for first.
The data block returns plain values; the presentation block only reads
them. Sample values keep the render reproducible, and each one notes the
command that supplies it on a real machine.
#>
param(
[ValidateSet('Light', 'Dark')]
[string] $Theme = 'Light',
[int] $Width = 1920,
[int] $Height = 1080,
[string] $OutputDirectory = (Join-Path $PSScriptRoot 'Output')
)
Import-Module (Join-Path $PSScriptRoot '..\PowerBGInfo.psd1') -Force
#region Data: your logic goes here
function Get-HelpDeskFacts {
[pscustomobject] @{
# (Get-CimInstance Win32_SystemEnclosure).SMBIOSAssetTag
AssetTag = 'CT-04217'
ComputerName = 'FRONTDESK-07'
Location = 'Reception, B1'
# (Get-NetIPConfiguration |
# Where-Object IPv4DefaultGateway).IPv4Address.IPAddress
IPAddress = '10.20.4.37'
Network = 'Wired · VLAN 120'
SignedInAs = 'Reception (shared)'
# (Get-CimInstance Win32_OperatingSystem).Caption and .BuildNumber
Windows = '11 Enterprise 24H2'
Build = '26100'
# Your monitoring or status-page API
Services = @(
@{ Text = 'Email, Teams, Wi-Fi and VPN are working'; Status = 'Ok' }
@{
Text = 'Floor 2 printers are offline until 14:00'
Status = 'Warning'
}
)
}
}
$fallback = [pscustomobject] @{
AssetTag = 'unknown'; ComputerName = $env:COMPUTERNAME
Location = 'unknown'; IPAddress = 'unknown'; Network = ''
SignedInAs = 'unknown'; Windows = 'unknown'; Build = ''
Services = @(@{ Text = 'Service status is unavailable'; Status = 'Info' })
}
$source = @{
Name = 'Help-desk facts'
TimeoutSeconds = 10
ScriptBlock = ${function:Get-HelpDeskFacts}
Default = $fallback
}
$facts = Invoke-BGInfoDataSource @source
#endregion
#region Presentation: reads only $facts
$scale = [Math]::Round($Height / 1080, 2)
$tile = @{ SurfaceStyle = 'Glass' }
$tiles = @(
New-BGInfoVisualCanvasTile @tile -IconKind Shield -Label 'ASSET TAG' `
-Value $facts.AssetTag -Detail 'Sticker under the keyboard'
New-BGInfoVisualCanvasTile @tile -IconKind Computer `
-Label 'COMPUTER NAME' -Value $facts.ComputerName `
-Detail $facts.Location
New-BGInfoVisualCanvasTile @tile -IconKind Network -Label 'IP ADDRESS' `
-Value $facts.IPAddress -Detail $facts.Network
New-BGInfoVisualCanvasTile @tile -IconKind User -Label 'SIGNED IN AS' `
-Value $facts.SignedInAs -Detail 'Resets at sign-out'
New-BGInfoVisualCanvasTile @tile -IconKind OperatingSystem `
-Label 'WINDOWS' -Value $facts.Windows -Detail "Build $($facts.Build)"
)
$look = New-BGInfoTheme Orchid -Mode $Theme -Scale $scale
New-BGInfo -Theme $look -TextPosition MiddleLeft -Target File {
New-BGInfoBackground -Width $Width -Height $Height
# The tile column sits vertically centered beside the centered text.
New-BGInfoLayout -ColumnAlignment Center
New-BGInfoLabel -Name 'IT SERVICE DESK' -Accent `
-FontSize ([int](16 * $scale)) -FontWeight 700
New-BGInfoLabel -Name 'Need help?' -FontSize ([int](88 * $scale)) `
-FontWeight 600
New-BGInfoLabel -Name 'Call 555-0100, extension 4357' -Accent `
-FontSize ([int](40 * $scale)) -FontWeight 600
New-BGInfoLabel -Name 'Have the asset tag on the right ready.' -Muted `
-FontSize ([int](22 * $scale))
New-BGInfoValue -Name 'Portal' -Value 'help.contoso.com'
New-BGInfoValue -Name 'Teams' -Value '#it-help'
New-BGInfoValue -Name 'Walk-in' -Value 'Building B, room 012'
New-BGInfoValue -Name 'Hours' -Value 'Monday–Friday, 07:00–19:00'
New-BGInfoLabel -Name ' ' -FontSize ([int](12 * $scale))
New-BGInfoLabel -Name 'SERVICE STATUS' -Accent `
-FontSize ([int](16 * $scale)) -FontWeight 700
foreach ($service in $facts.Services) {
New-BGInfoLabel -Name "● $($service.Text)" -Status $service.Status `
-FontSize ([int](19 * $scale)) -FontWeight 600
}
New-BGInfoVisualCanvas -Region Right -Tile $tiles
} -ConfigurationDirectory $OutputDirectory `
-OutputFileName "helpdesk-$($Theme.ToLower()).png"
#endregion
Meeting room
Which room, is it free, and how to share a screen.
New-BGInfo -Theme $look -Target File {
New-BGInfoBackground -Width $Width -Height $Height -Pattern None
New-BGInfoLabel -Name $room.Where -Accent -FontSize ([int](15 * $scale)) `
-FontWeight 700
New-BGInfoLabel -Name $room.Room -FontSize ([int](150 * $scale)) `
-FontWeight 700
New-BGInfoLabel -Name "● $($status.Text)" -Status $status.Status `
-FontSize ([int](34 * $scale)) -FontWeight 600
New-BGInfoLabel -Name ' ' -FontSize ([int](8 * $scale))
New-BGInfoValue -Name 'Next booking' -Value $after
New-BGInfoValue -Name 'Room' -Value $room.Seats
New-BGInfoValue -Name 'Book it' -Value $room.Book
New-BGInfoValue -Name 'Help' -Value 'ext. 4357, or help.contoso.com'
# Right column: guest Wi-Fi on the phone, then how to share a screen.
New-BGInfoQRCode -Region Right -Text $room.Wifi.Text -Title 'Guest Wi-Fi' `
-Caption $room.Wifi.Ssid -Note 'scan to join' `
-Height ([int](330 * $scale))
New-BGInfoList -Region Right -Title 'Share your screen' -Marker Number `
-Item $room.Steps
# Bottom band: today in this room and the two next to it.
New-BGInfoTimeline -Region Bottom -Title 'Today' -NoLegend `
-Note 'Bergen and Tromsø are next door' `
-Item $day -Now $room.Now -Height ([int](280 * $scale))
} -ConfigurationDirectory $OutputDirectory `
-OutputFileName "meeting-room-$($Theme.ToLower()).png"
#endregion
#region Data: your logic goes here
function Get-MeetingRoomData {
$today = Get-Date '2026-10-16'
# Microsoft Graph: GET /users/{room}/calendarView for today, for this
# room and its neighbours. One row per booking.
function Booking($room, $from, $to) {
$kind = if ($room -eq 'Oslo') { 'This room' } else { 'Nearby rooms' }
[pscustomobject] @{
Lane = $room; Category = $kind
Start = $today.Add([timespan] $from)
End = $today.Add([timespan] $to)
}
}
[pscustomobject] @{
Available = $true
Room = 'Oslo'
Where = 'BUILDING A · FLOOR 4 · ROOM 4.12'
Seats = '10 seats · video bar · whiteboard'
Book = 'Outlook or Teams, room "Oslo 4.12"'
Now = $today.AddHours(11).AddMinutes(50)
Bookings = @(
Booking 'Oslo' '08:30' '09:30'
Booking 'Oslo' '10:00' '11:30'
Booking 'Oslo' '13:00' '14:00'
Booking 'Oslo' '15:00' '16:30'
Booking 'Bergen' '09:00' '12:30'
Booking 'Bergen' '14:00' '15:00'
Booking 'Tromsø' '08:00' '10:00'
Booking 'Tromsø' '11:00' '17:00'
)
# The guest network: WIFI:T:<WPA|nopass>;S:<SSID>;P:<key>;;
Wifi = @{
Ssid = 'CONTOSO-GUEST'
Text = 'WIFI:T:nopass;S:CONTOSO-GUEST;;'
}
Steps = @(
'Plug the USB-C cable on the table into your laptop'
'Press Share on the touch panel'
'No cable? In Teams choose Cast, then Oslo'
'The screen turns off 5 minutes after you leave'
)
}
}
$fallback = [pscustomobject] @{
Available = $false
Room = $env:COMPUTERNAME; Where = 'MEETING ROOM'; Seats = ''
Book = 'Outlook or Teams'; Now = Get-Date; Bookings = @()
Wifi = @{ Ssid = 'CONTOSO-GUEST'; Text = 'WIFI:T:nopass;S:CONTOSO-GUEST;;' }
Steps = @('Call the service desk on ext. 4357')
}
$source = @{
Name = 'Meeting room data'
TimeoutSeconds = 10
ScriptBlock = ${function:Get-MeetingRoomData}
Default = $fallback
}
$room = Invoke-BGInfoDataSource @source
#endregion
Full script 138 lines
<#
Meeting room PC. The question for someone walking in: which room is
this, is it free, and how do I get my laptop on the screen?
The data block returns plain values; the presentation block only reads
them. Comments name the command or source that supplies each value.
#>
param(
[ValidateSet('Light', 'Dark')]
[string] $Theme = 'Dark',
[int] $Width = 1920,
[int] $Height = 1080,
[string] $OutputDirectory = (Join-Path $PSScriptRoot 'Output')
)
Import-Module (Join-Path $PSScriptRoot '..\PowerBGInfo.psd1') -Force
#region Data: your logic goes here
function Get-MeetingRoomData {
$today = Get-Date '2026-10-16'
# Microsoft Graph: GET /users/{room}/calendarView for today, for this
# room and its neighbours. One row per booking.
function Booking($room, $from, $to) {
$kind = if ($room -eq 'Oslo') { 'This room' } else { 'Nearby rooms' }
[pscustomobject] @{
Lane = $room; Category = $kind
Start = $today.Add([timespan] $from)
End = $today.Add([timespan] $to)
}
}
[pscustomobject] @{
Available = $true
Room = 'Oslo'
Where = 'BUILDING A · FLOOR 4 · ROOM 4.12'
Seats = '10 seats · video bar · whiteboard'
Book = 'Outlook or Teams, room "Oslo 4.12"'
Now = $today.AddHours(11).AddMinutes(50)
Bookings = @(
Booking 'Oslo' '08:30' '09:30'
Booking 'Oslo' '10:00' '11:30'
Booking 'Oslo' '13:00' '14:00'
Booking 'Oslo' '15:00' '16:30'
Booking 'Bergen' '09:00' '12:30'
Booking 'Bergen' '14:00' '15:00'
Booking 'Tromsø' '08:00' '10:00'
Booking 'Tromsø' '11:00' '17:00'
)
# The guest network: WIFI:T:<WPA|nopass>;S:<SSID>;P:<key>;;
Wifi = @{
Ssid = 'CONTOSO-GUEST'
Text = 'WIFI:T:nopass;S:CONTOSO-GUEST;;'
}
Steps = @(
'Plug the USB-C cable on the table into your laptop'
'Press Share on the touch panel'
'No cable? In Teams choose Cast, then Oslo'
'The screen turns off 5 minutes after you leave'
)
}
}
$fallback = [pscustomobject] @{
Available = $false
Room = $env:COMPUTERNAME; Where = 'MEETING ROOM'; Seats = ''
Book = 'Outlook or Teams'; Now = Get-Date; Bookings = @()
Wifi = @{ Ssid = 'CONTOSO-GUEST'; Text = 'WIFI:T:nopass;S:CONTOSO-GUEST;;' }
Steps = @('Call the service desk on ext. 4357')
}
$source = @{
Name = 'Meeting room data'
TimeoutSeconds = 10
ScriptBlock = ${function:Get-MeetingRoomData}
Default = $fallback
}
$room = Invoke-BGInfoDataSource @source
#endregion
#region Presentation: reads only $room
$scale = [Math]::Round($Height / 1080, 2)
$look = New-BGInfoTheme Orchid -Mode $Theme -Scale $scale
$here = @($room.Bookings | Where-Object Lane -EQ $room.Room)
$current = $here |
Where-Object { $_.Start -le $room.Now -and $_.End -gt $room.Now }
$next = $here | Where-Object Start -GT $room.Now | Sort-Object Start |
Select-Object -First 1
$status = if (-not $room.Available) {
@{ Text = 'Availability unavailable'; Status = 'Info' }
} elseif ($current) {
@{
Text = "In use until $($current.End.ToString('HH:mm'))"
Status = 'Warning'
}
} elseif ($next) {
@{ Text = "Free until $($next.Start.ToString('HH:mm'))"; Status = 'Ok' }
} else {
@{ Text = 'Free for the rest of the day'; Status = 'Ok' }
}
$after = if (-not $room.Available) { 'unavailable' } elseif ($next) {
"$($next.Start.ToString('HH:mm'))–$($next.End.ToString('HH:mm'))"
} else { 'none today' }
# This room takes the accent; the rooms next door stay quiet.
$quiet = $look.Muted.WithAlpha(150).ToHexRgba()
$day = $room.Bookings | Select-Object *, @{
Name = 'Color'
Expression = {
if ($_.Lane -eq $room.Room) { $look.Accent.ToHex() } else { $quiet }
}
}
New-BGInfo -Theme $look -Target File {
New-BGInfoBackground -Width $Width -Height $Height -Pattern None
New-BGInfoLabel -Name $room.Where -Accent -FontSize ([int](15 * $scale)) `
-FontWeight 700
New-BGInfoLabel -Name $room.Room -FontSize ([int](150 * $scale)) `
-FontWeight 700
New-BGInfoLabel -Name "● $($status.Text)" -Status $status.Status `
-FontSize ([int](34 * $scale)) -FontWeight 600
New-BGInfoLabel -Name ' ' -FontSize ([int](8 * $scale))
New-BGInfoValue -Name 'Next booking' -Value $after
New-BGInfoValue -Name 'Room' -Value $room.Seats
New-BGInfoValue -Name 'Book it' -Value $room.Book
New-BGInfoValue -Name 'Help' -Value 'ext. 4357, or help.contoso.com'
# Right column: guest Wi-Fi on the phone, then how to share a screen.
New-BGInfoQRCode -Region Right -Text $room.Wifi.Text -Title 'Guest Wi-Fi' `
-Caption $room.Wifi.Ssid -Note 'scan to join' `
-Height ([int](330 * $scale))
New-BGInfoList -Region Right -Title 'Share your screen' -Marker Number `
-Item $room.Steps
# Bottom band: today in this room and the two next to it.
New-BGInfoTimeline -Region Bottom -Title 'Today' -NoLegend `
-Note 'Bergen and Tromsø are next door' `
-Item $day -Now $room.Now -Height ([int](280 * $scale))
} -ConfigurationDirectory $OutputDirectory `
-OutputFileName "meeting-room-$($Theme.ToLower()).png"
#endregion
More wallpapers
Filter by who the screen is for. Each card opens the wallpaper in the page theme with the same Presentation and Data tabs, and links to the full-size PNG and the script on GitHub.
Any screen
A script lays out for the screen it renders on. The on-call board spreads across a 3440 x 1440 ultrawide in the operations room, and the signage script turns a 1080 x 1920 portrait panel into a poster. Each file is drawn at its size, not scaled from 1920 x 1080.
Rendered by Examples/Run.BGInfo.Showcase.ps1 from PowerBGInfo 2f6f22d, every scenario at 1920 x 1080 and these two also at their own screen sizes. Every wallpaper on this page is a script's output, unedited.
Build one yourself
One wallpaper in eight steps. Each step shows only the lines it changes, next to the wallpaper the whole script renders at that point. Every background is generated: there is no stock image underneath, only a gradient, glows and a pattern that ChartForgeX paints at the screen's size.
Start with an empty canvas
New-BGInfoBackground sets the size and paints one colour. Without it, PowerBGInfo draws on -FilePath or on the wallpaper that is already set, and never on its own earlier output.
PowerShell01-canvas.ps111 lines param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 Color = '#0B1222' } New-BGInfo -Target File { New-BGInfoBackground @backdrop } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'01-canvas.png1920 x 1080 
Generate the background in layers
Three keys on the same hashtable add one layer each. The backdrop is painted at the size you ask for, so it is never a stretched photo.
Gradient. Two colours blend across the canvas.
Whole script after this step 11 lines
PowerShell02-gradient.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 Gradient = '#0B3B46', '#050A14' } New-BGInfo -Target File { New-BGInfoBackground @backdrop } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'Glows. Soft radial light at relative positions, so it lands in the same place at every resolution.
PowerShell03-glows.ps1+4 −0 Glow = @( New-BGInfoBackgroundGlow -X 0.85 -Y 0.1 -Radius 0.6 -Color '#2DD4BF40' New-BGInfoBackgroundGlow -X 0.05 -Y 0.95 -Radius 0.5 -Color '#3B82F633' )03-glows.png1920 x 1080 
Whole script after this step 15 lines
PowerShell03-glows.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 Gradient = '#0B3B46', '#050A14' Glow = @( New-BGInfoBackgroundGlow -X 0.85 -Y 0.1 -Radius 0.6 -Color '#2DD4BF40' New-BGInfoBackgroundGlow -X 0.05 -Y 0.95 -Radius 0.5 -Color '#3B82F633' ) } New-BGInfo -Target File { New-BGInfoBackground @backdrop } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'Pattern. A grid on top. Dots work the same way.
Whole script after this step 17 lines
PowerShell04-pattern.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 Gradient = '#0B3B46', '#050A14' Glow = @( New-BGInfoBackgroundGlow -X 0.85 -Y 0.1 -Radius 0.6 -Color '#2DD4BF40' New-BGInfoBackgroundGlow -X 0.05 -Y 0.95 -Radius 0.5 -Color '#3B82F633' ) Pattern = 'Grid' PatternSpacing = 64 } New-BGInfo -Target File { New-BGInfoBackground @backdrop } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'Name the machine
Labels and name and value pairs form the text block in the top-left corner. -BuiltinValue HostName, OSName or IPv4Address read the machine the script runs on; this page uses fixed values so anyone can reproduce the render.
PowerShell05-details.ps1+8 −1 New-BGInfoLabel -Name 'FINANCE · FLOOR 3' -Color '#2DD4BF' -FontWeight 700 New-BGInfoLabel -Name 'WS-FIN-0042' -Color White -FontSize 64 -Bold New-BGInfoValue -Name 'User' -Value 'CONTOSO\anowak' New-BGInfoValue -Name 'Windows' -Value 'Windows 11 Enterprise' New-BGInfoValue -Name 'IP address' -Value '10.20.4.37' New-BGInfoValue -Name 'Support' -Value 'ext. 4357' } -Color '#94A3B8' -ValueColor White -FontSize 18 -SpaceX 64 -SpaceY 56 ` -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'05-details.png1920 x 1080 
Whole script after this step 25 lines
PowerShell05-details.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 Gradient = '#0B3B46', '#050A14' Glow = @( New-BGInfoBackgroundGlow -X 0.85 -Y 0.1 -Radius 0.6 -Color '#2DD4BF40' New-BGInfoBackgroundGlow -X 0.05 -Y 0.95 -Radius 0.5 -Color '#3B82F633' ) Pattern = 'Grid' PatternSpacing = 64 } New-BGInfo -Target File { New-BGInfoBackground @backdrop New-BGInfoLabel -Name 'FINANCE · FLOOR 3' -Color '#2DD4BF' -FontWeight 700 New-BGInfoLabel -Name 'WS-FIN-0042' -Color White -FontSize 64 -Bold New-BGInfoValue -Name 'User' -Value 'CONTOSO\anowak' New-BGInfoValue -Name 'Windows' -Value 'Windows 11 Enterprise' New-BGInfoValue -Name 'IP address' -Value '10.20.4.37' New-BGInfoValue -Name 'Support' -Value 'ext. 4357' } -Color '#94A3B8' -ValueColor White -FontSize 18 -SpaceX 64 -SpaceY 56 ` -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'Pick a theme preset
-Theme Aurora gives the text, the panels and the backdrop one look, so the hand-picked colours and the background layers go. Parameters you set still win, and -ThemeMode Light gives the light pair.
PowerShell06-theme.ps1+4 −12 New-BGInfo -Theme Aurora -ThemeMode Dark -Target File { New-BGInfoLabel -Name 'FINANCE · FLOOR 3' -Accent -FontWeight 700 New-BGInfoLabel -Name 'WS-FIN-0042' -FontSize 64 -Bold } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'06-theme.png1920 x 1080 
Whole script after this step 17 lines
PowerShell06-theme.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 } New-BGInfo -Theme Aurora -ThemeMode Dark -Target File { New-BGInfoBackground @backdrop New-BGInfoLabel -Name 'FINANCE · FLOOR 3' -Accent -FontWeight 700 New-BGInfoLabel -Name 'WS-FIN-0042' -FontSize 64 -Bold New-BGInfoValue -Name 'User' -Value 'CONTOSO\anowak' New-BGInfoValue -Name 'Windows' -Value 'Windows 11 Enterprise' New-BGInfoValue -Name 'IP address' -Value '10.20.4.37' New-BGInfoValue -Name 'Support' -Value 'ext. 4357' } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'Add the first chart
A chart panel placed by anchor and offsets, the way PowerBGInfo has always placed them. It picks up the theme's panel, type and series colour without a single colour parameter.
PowerShell07-chart.ps1+3 −0 New-BGInfoChart -Title 'CPU' -Kind Area -Values 12, 18, 25, 22, 30, 27 ` -ValueSuffix ' %' -NoHistory ` -Anchor TopRight -OffsetX 64 -OffsetY 56 -Width 460 -Height 24007-chart.png1920 x 1080 
Whole script after this step 21 lines
PowerShell07-chart.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 } New-BGInfo -Theme Aurora -ThemeMode Dark -Target File { New-BGInfoBackground @backdrop New-BGInfoLabel -Name 'FINANCE · FLOOR 3' -Accent -FontWeight 700 New-BGInfoLabel -Name 'WS-FIN-0042' -FontSize 64 -Bold New-BGInfoValue -Name 'User' -Value 'CONTOSO\anowak' New-BGInfoValue -Name 'Windows' -Value 'Windows 11 Enterprise' New-BGInfoValue -Name 'IP address' -Value '10.20.4.37' New-BGInfoValue -Name 'Support' -Value 'ext. 4357' New-BGInfoChart -Title 'CPU' -Kind Area -Values 12, 18, 25, 22, 30, 27 ` -ValueSuffix ' %' -NoHistory ` -Anchor TopRight -OffsetX 64 -OffsetY 56 -Width 460 -Height 240 } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'Let regions do the layout
-Region Right stacks panels in a column and -Region Bottom shares a band. Nobody computes widths, the taskbar stays clear, and the layout follows the screen size; the region renders further down show it at ultrawide and portrait sizes.
PowerShell08-regions.ps1+13 −1 -Region Right New-BGInfoMetric -Region Right -Label 'Memory' -Value 86 -Unit '%' ` -Caption 'of 32 GB' -Height 150 New-BGInfoChart -Region Bottom -Title 'Volumes used' -Kind ProgressBar ` -Values 63, 93 -Labels 'C: System', 'D: Data' -Maximum 100 ` -ValueSuffix ' %' -NoProgressHandles -ShowLatestValue:$false ` -NoHistory New-BGInfoChart -Region Bottom -Title 'Network' -Kind Line ` -Values 120, 180, 160, 220, 260, 240 -ValueSuffix ' Mbps' -NoHistory New-BGInfoList -Region Bottom -Title 'This week' -Item @( @{ Text = 'Month-end close'; Checked = $false; Value = 'Fri' } @{ Text = 'Backups verified'; Checked = $true; Value = '02:10' } )08-regions.png1920 x 1080 
Whole script after this step 34 lines
PowerShell08-regions.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 } New-BGInfo -Theme Aurora -ThemeMode Dark -Target File { New-BGInfoBackground @backdrop New-BGInfoLabel -Name 'FINANCE · FLOOR 3' -Accent -FontWeight 700 New-BGInfoLabel -Name 'WS-FIN-0042' -FontSize 64 -Bold New-BGInfoValue -Name 'User' -Value 'CONTOSO\anowak' New-BGInfoValue -Name 'Windows' -Value 'Windows 11 Enterprise' New-BGInfoValue -Name 'IP address' -Value '10.20.4.37' New-BGInfoValue -Name 'Support' -Value 'ext. 4357' New-BGInfoChart -Title 'CPU' -Kind Area -Values 12, 18, 25, 22, 30, 27 ` -ValueSuffix ' %' -NoHistory ` -Region Right New-BGInfoMetric -Region Right -Label 'Memory' -Value 86 -Unit '%' ` -Caption 'of 32 GB' -Height 150 New-BGInfoChart -Region Bottom -Title 'Volumes used' -Kind ProgressBar ` -Values 63, 93 -Labels 'C: System', 'D: Data' -Maximum 100 ` -ValueSuffix ' %' -NoProgressHandles -ShowLatestValue:$false ` -NoHistory New-BGInfoChart -Region Bottom -Title 'Network' -Kind Line ` -Values 120, 180, 160, 220, 260, 240 -ValueSuffix ' Mbps' -NoHistory New-BGInfoList -Region Bottom -Title 'This week' -Item @( @{ Text = 'Month-end close'; Checked = $false; Value = 'Fri' } @{ Text = 'Backups verified'; Checked = $true; Value = '02:10' } ) } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'Add thresholds
Warning and critical thresholds turn the memory card amber and the D: bar red by themselves, using the theme's status colours.
PowerShell09-thresholds.ps1+3 −2 -Caption 'of 32 GB' -Height 150 ` -WarningThreshold 80 -CriticalThreshold 90 -NoHistory -WarningThreshold 80 -CriticalThreshold 9009-thresholds.png1920 x 1080 
Whole script after this step 35 lines
PowerShell09-thresholds.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") $backdrop = @{ Width = 1920 Height = 1080 } New-BGInfo -Theme Aurora -ThemeMode Dark -Target File { New-BGInfoBackground @backdrop New-BGInfoLabel -Name 'FINANCE · FLOOR 3' -Accent -FontWeight 700 New-BGInfoLabel -Name 'WS-FIN-0042' -FontSize 64 -Bold New-BGInfoValue -Name 'User' -Value 'CONTOSO\anowak' New-BGInfoValue -Name 'Windows' -Value 'Windows 11 Enterprise' New-BGInfoValue -Name 'IP address' -Value '10.20.4.37' New-BGInfoValue -Name 'Support' -Value 'ext. 4357' New-BGInfoChart -Title 'CPU' -Kind Area -Values 12, 18, 25, 22, 30, 27 ` -ValueSuffix ' %' -NoHistory ` -Region Right New-BGInfoMetric -Region Right -Label 'Memory' -Value 86 -Unit '%' ` -Caption 'of 32 GB' -Height 150 ` -WarningThreshold 80 -CriticalThreshold 90 New-BGInfoChart -Region Bottom -Title 'Volumes used' -Kind ProgressBar ` -Values 63, 93 -Labels 'C: System', 'D: Data' -Maximum 100 ` -ValueSuffix ' %' -NoProgressHandles -ShowLatestValue:$false ` -NoHistory -WarningThreshold 80 -CriticalThreshold 90 New-BGInfoChart -Region Bottom -Title 'Network' -Kind Line ` -Values 120, 180, 160, 220, 260, 240 -ValueSuffix ' Mbps' -NoHistory New-BGInfoList -Region Bottom -Title 'This week' -Item @( @{ Text = 'Month-end close'; Checked = $false; Value = 'Fri' } @{ Text = 'Backups verified'; Checked = $true; Value = '02:10' } ) } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png'Bring your own data
The values move into one function, Invoke-BGInfoDataSource runs it with a time limit and a fallback, and the drawing code only reads $data. The wallpaper looks the same; now it is yours.
PowerShell10-data.ps1+49 −14 #region Data: your logic goes here function Get-MyWallpaperData { # Sample values. Each comment names a live source for the same value. [pscustomobject] @{ Team = 'FINANCE · FLOOR 3' Name = 'WS-FIN-0042' # $env:COMPUTERNAME User = 'CONTOSO\anowak' # Win32_ComputerSystem.UserName Windows = 'Windows 11 Enterprise' # Win32_OperatingSystem.Caption IPv4 = '10.20.4.37' # Get-NetIPAddress Cpu = 12, 18, 25, 22, 30, 27 # Get-Counter, or your monitoring Memory = 86 # Win32_OperatingSystem, % in use # Get-Volume | Where-Object DriveLetter, % used per volume Volumes = [ordered] @{ 'C: System' = 63; 'D: Data' = 93 } Network = 120, 180, 160, 220, 260, 240 Tasks = @( @{ Text = 'Month-end close'; Checked = $false; Value = 'Fri' } @{ Text = 'Backups verified'; Checked = $true; Value = '02:10' } ) } } # What the wallpaper shows when the source fails or runs out of time. $fallback = [pscustomobject] @{ Team = 'STATUS UNAVAILABLE'; Name = $env:COMPUTERNAME User = 'unknown'; Windows = 'unknown'; IPv4 = 'unknown' Cpu = @(0); Memory = 0; Volumes = @{ 'C:' = 0 }; Network = @(0) Tasks = @(@{ Text = 'Status unknown'; Checked = $false }) } $source = @{ Name = 'Wallpaper data' TimeoutSeconds = 10 ScriptBlock = ${function:Get-MyWallpaperData} Default = $fallback } $data = Invoke-BGInfoDataSource @source #endregion #region Presentation: reads only $data New-BGInfoLabel -Name $data.Team -Accent -FontWeight 700 New-BGInfoLabel -Name $data.Name -FontSize 64 -Bold New-BGInfoValue -Name 'User' -Value $data.User New-BGInfoValue -Name 'Windows' -Value $data.Windows New-BGInfoValue -Name 'IP address' -Value $data.IPv4 New-BGInfoChart -Title 'CPU' -Kind Area -Values $data.Cpu ` New-BGInfoMetric -Region Right -Label 'Memory' -Unit '%' ` -Value $data.Memory -Caption 'of 32 GB' -Height 150 ` -Values @($data.Volumes.Values) -Labels @($data.Volumes.Keys) ` -Maximum 100 ` -Values $data.Network -ValueSuffix ' Mbps' -NoHistory New-BGInfoList -Region Bottom -Title 'This week' -Item $data.Tasks #endregion10-data.png1920 x 1080 
Whole script after this step 72 lines
PowerShell10-data.ps1 param([string] $OutputDirectory = "$env:TEMP\PowerBGInfo") #region Data: your logic goes here function Get-MyWallpaperData { # Sample values. Each comment names a live source for the same value. [pscustomobject] @{ Team = 'FINANCE · FLOOR 3' Name = 'WS-FIN-0042' # $env:COMPUTERNAME User = 'CONTOSO\anowak' # Win32_ComputerSystem.UserName Windows = 'Windows 11 Enterprise' # Win32_OperatingSystem.Caption IPv4 = '10.20.4.37' # Get-NetIPAddress Cpu = 12, 18, 25, 22, 30, 27 # Get-Counter, or your monitoring Memory = 86 # Win32_OperatingSystem, % in use # Get-Volume | Where-Object DriveLetter, % used per volume Volumes = [ordered] @{ 'C: System' = 63; 'D: Data' = 93 } Network = 120, 180, 160, 220, 260, 240 Tasks = @( @{ Text = 'Month-end close'; Checked = $false; Value = 'Fri' } @{ Text = 'Backups verified'; Checked = $true; Value = '02:10' } ) } } # What the wallpaper shows when the source fails or runs out of time. $fallback = [pscustomobject] @{ Team = 'STATUS UNAVAILABLE'; Name = $env:COMPUTERNAME User = 'unknown'; Windows = 'unknown'; IPv4 = 'unknown' Cpu = @(0); Memory = 0; Volumes = @{ 'C:' = 0 }; Network = @(0) Tasks = @(@{ Text = 'Status unknown'; Checked = $false }) } $source = @{ Name = 'Wallpaper data' TimeoutSeconds = 10 ScriptBlock = ${function:Get-MyWallpaperData} Default = $fallback } $data = Invoke-BGInfoDataSource @source #endregion #region Presentation: reads only $data $backdrop = @{ Width = 1920 Height = 1080 } New-BGInfo -Theme Aurora -ThemeMode Dark -Target File { New-BGInfoBackground @backdrop New-BGInfoLabel -Name $data.Team -Accent -FontWeight 700 New-BGInfoLabel -Name $data.Name -FontSize 64 -Bold New-BGInfoValue -Name 'User' -Value $data.User New-BGInfoValue -Name 'Windows' -Value $data.Windows New-BGInfoValue -Name 'IP address' -Value $data.IPv4 New-BGInfoValue -Name 'Support' -Value 'ext. 4357' New-BGInfoChart -Title 'CPU' -Kind Area -Values $data.Cpu ` -ValueSuffix ' %' -NoHistory ` -Region Right New-BGInfoMetric -Region Right -Label 'Memory' -Unit '%' ` -Value $data.Memory -Caption 'of 32 GB' -Height 150 ` -WarningThreshold 80 -CriticalThreshold 90 New-BGInfoChart -Region Bottom -Title 'Volumes used' -Kind ProgressBar ` -Values @($data.Volumes.Values) -Labels @($data.Volumes.Keys) ` -Maximum 100 ` -ValueSuffix ' %' -NoProgressHandles -ShowLatestValue:$false ` -NoHistory -WarningThreshold 80 -CriticalThreshold 90 New-BGInfoChart -Region Bottom -Title 'Network' -Kind Line ` -Values $data.Network -ValueSuffix ' Mbps' -NoHistory New-BGInfoList -Region Bottom -Title 'This week' -Item $data.Tasks } -ConfigurationDirectory $OutputDirectory -OutputFileName 'wallpaper.png' #endregion
The steps are scripts in this site's repository, rendered with -Target File by PowerBGInfo built from source commit 2f6f22d.
Your logic, your data
Collecting the values is the part only you can write, so PowerBGInfo keeps it apart from the drawing. Every example has the same two regions: a data block that returns plain values, and a presentation block that only reads them.
The data block
One function returns one object. This is where your sources go: replace the sample values, keep the property names, and the layout keeps working. A comment beside each value names a live source for it.
#region Data: your logic goes here
function Get-MyWallpaperData {
# Sample values. Each comment names a live source for the same value.
[pscustomobject] @{
Team = 'FINANCE · FLOOR 3'
Name = 'WS-FIN-0042' # $env:COMPUTERNAME
User = 'CONTOSO\anowak' # Win32_ComputerSystem.UserName
Windows = 'Windows 11 Enterprise' # Win32_OperatingSystem.Caption
IPv4 = '10.20.4.37' # Get-NetIPAddress
Cpu = 12, 18, 25, 22, 30, 27 # Get-Counter, or your monitoring
Memory = 86 # Win32_OperatingSystem, % in use
# Get-Volume | Where-Object DriveLetter, % used per volume
Volumes = [ordered] @{ 'C: System' = 63; 'D: Data' = 93 }
Network = 120, 180, 160, 220, 260, 240
Tasks = @(
@{ Text = 'Month-end close'; Checked = $false; Value = 'Fri' }
@{ Text = 'Backups verified'; Checked = $true; Value = '02:10' }
)
}
}
# What the wallpaper shows when the source fails or runs out of time.
$fallback = [pscustomobject] @{
Team = 'STATUS UNAVAILABLE'; Name = $env:COMPUTERNAME
User = 'unknown'; Windows = 'unknown'; IPv4 = 'unknown'
Cpu = @(0); Memory = 0; Volumes = @{ 'C:' = 0 }; Network = @(0)
Tasks = @(@{ Text = 'Status unknown'; Checked = $false })
}
$source = @{
Name = 'Wallpaper data'
TimeoutSeconds = 10
ScriptBlock = ${function:Get-MyWallpaperData}
Default = $fallback
}
$data = Invoke-BGInfoDataSource @source
#endregion
Typical sources
Each snippet is a body for that function. Combine them as you need; the wallpaper only sees the object they return.
Operating system, memory, uptime and fixed disks from Get-CimInstance. Read-only, and no module to install.
function Get-MyWallpaperData {
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$disks = Get-CimInstance -ClassName Win32_LogicalDisk `
-Filter 'DriveType = 3'
$uptime = (Get-Date) - $os.LastBootUpTime
$free = $os.FreePhysicalMemory / $os.TotalVisibleMemorySize
$volumes = [ordered] @{}
foreach ($disk in $disks) {
$used = 1 - $disk.FreeSpace / $disk.Size
$volumes[$disk.DeviceID] = [int] (100 * $used)
}
[pscustomobject] @{
Windows = $os.Caption
Uptime = '{0} days {1} h' -f $uptime.Days, $uptime.Hours
Memory = [int] (100 * (1 - $free))
Volumes = $volumes
}
}
The Windows release and build, and values your build or asset process wrote under its own key.
function Get-MyWallpaperData {
$key = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$windows = Get-ItemProperty -Path $key
# Your own values, written by the build or the asset process.
$asset = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Contoso\Asset' `
-ErrorAction SilentlyContinue
[pscustomobject] @{
Release = $windows.DisplayVersion
Build = '{0}.{1}' -f $windows.CurrentBuild, $windows.UBR
Team = if ($asset) { $asset.Department } else { 'unassigned' }
AssetTag = if ($asset) { $asset.Tag } else { 'unknown' }
}
}
The signed-in user's department and title, and the computer's description and owner, from the ActiveDirectory module.
function Get-MyWallpaperData {
# Needs the RSAT ActiveDirectory module and a domain controller in reach.
Import-Module ActiveDirectory -ErrorAction Stop
$user = Get-ADUser -Identity $env:USERNAME -Properties Department, Title
$computer = Get-ADComputer -Identity $env:COMPUTERNAME `
-Properties Description, ManagedBy
[pscustomobject] @{
User = $user.Name
Team = '{0} · {1}' -f $user.Department, $user.Title
Role = $computer.Description
Owner = if ($computer.ManagedBy) {
(Get-ADObject -Identity $computer.ManagedBy).Name
} else { 'not set' }
}
}
A CMDB or ticketing API through Invoke-RestMethod. The function runs in its own runspace, so it gets the computer name through -ArgumentList.
function Get-MyWallpaperData {
param([string] $ComputerName)
$uri = "https://cmdb.contoso.com/api/devices/$ComputerName"
$device = Invoke-RestMethod -Uri $uri -UseDefaultCredentials -TimeoutSec 5
[pscustomobject] @{
Owner = $device.owner
Team = $device.costCenter
Tickets = $device.openTickets
}
}
# The function runs in its own runspace: pass in what it needs.
$source = @{
Name = 'CMDB'
TimeoutSeconds = 10
ScriptBlock = ${function:Get-MyWallpaperData}
ArgumentList = $env:COMPUTERNAME
Default = $fallback
}
$data = Invoke-BGInfoDataSource @source
JSON and CSV files that a monitoring tool already exports, read with ConvertFrom-Json and Import-Csv.
function Get-MyWallpaperData {
# The monitoring tool exports one JSON and one CSV file per machine.
$share = '\\mon01.contoso.com\exports'
$status = Get-Content -Path "$share\$env:COMPUTERNAME.json" -Raw |
ConvertFrom-Json
$checks = Import-Csv -Path "$share\$env:COMPUTERNAME-checks.csv"
[pscustomobject] @{
Cpu = $status.cpu.history # numbers, oldest first
Memory = $status.memory.percent
Tasks = foreach ($check in $checks) {
@{ Text = $check.Name; Checked = $check.State -eq 'OK' }
}
}
}
A slow source never blocks logon
Invoke-BGInfoDataSource runs your function in its own runspace with a time limit. When the function throws, returns nothing or runs out of time, it writes a warning and returns the fallback you gave it, so the wallpaper always renders and sign-in never waits on a broken CMDB.
- Keep -TimeoutSeconds small, 5 to 15 seconds. A timed-out runspace is stopped in the background and the call returns at the limit.
- The function cannot see your script's variables. Pass what it needs with -ArgumentList and read it with param().
- Give each slow source its own call, so one failure only replaces its own values.
When the source does not answer. The same script with a source that hangs. Invoke-BGInfoDataSource stops waiting after ten seconds, writes a warning and returns the fallback, and the wallpaper still renders.
Swap a built-in helper
Get-BGInfoSystemInfo returns uptime, OS and build, IPv4, fixed disks, the last update, Defender and BitLocker state and the console user as one plain object, and every probe in it is read-only. A function of your own that returns the same property names replaces it; the system facts example does exactly that with sample values.
The same facts are built-in tokens for New-BGInfoValue -BuiltinValue:
# The built-in helper: read-only, each probe limited to five seconds.
$facts = Get-BGInfoSystemInfo
# Your own function with the same property names drops in for it.
function Get-MySystemInfo {
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
$volumes = Get-Volume | Where-Object { $_.DriveLetter -and $_.Size }
[pscustomobject] @{
ComputerName = $env:COMPUTERNAME
OSName = $os.Caption
UptimeText = '{0} days {1} h' -f $uptime.Days, $uptime.Hours
Disks = foreach ($volume in $volumes) {
[pscustomobject] @{
Drive = "$($volume.DriveLetter):"
FreeGB = [Math]::Round($volume.SizeRemaining / 1GB, 1)
UsedPercent = [Math]::Round(
100 * (1 - $volume.SizeRemaining / $volume.Size), 1)
}
}
}
}
$facts = Get-MySystemInfo
# The presentation code reads the same names either way.
$facts.Disks | New-BGInfoTable -Region Right -Title 'Disks' `
-Property Drive, FreeGB, UsedPercent

Themes, regions and widgets
Six theme presets give a wallpaper one look in one parameter, five regions lay it out on any screen, and eight widget cmdlets draw what charts alone cannot.
Six theme presets
Each preset has a light and a dark pair; the page theme picks which one you see. New-BGInfoTheme adjusts a preset's accent, scale or font, and parameters you set always win. Feature.Themes.ps1
New-BGInfo -Theme Midnight -ThemeMode Light { ... }Regions on every screen
One script rendered at three sizes. Left and Right are columns, Top and Bottom are bands and Center sits between them; a panel without a size shares what is left. New-BGInfoLayout keeps the taskbar and the desktop icons clear. Feature.Regions.ps1
New-BGInfoLayout -IconAreaWidth 96 -Gap 24Widgets
Each widget is one cmdlet that takes plain values, hashtables or the pipeline, and flows into a region like any chart. The crops come from the feature examples; open one for the whole wallpaper. Examples/Features
What ChartForgeX draws
PowerBGInfo collects the values, lays out the wallpaper and applies it through Windows. Every pixel of the image comes from ChartForgeX; there is no System.Drawing or browser underneath. A theme preset resolves into the same ChartForgeX light or dark mode for every part, so a wallpaper reads as one design instead of a stack of overlays.
Charts
New-BGInfoChartThirteen chart kinds, from sparklines and gauges to progress bars and pictorial rows. Thresholds colour each bar, row or gauge by its value.
Visual blocks
New-BGInfoMetricMetric cards, tables and lists from ChartForgeX visual blocks, for exact facts that do not belong in a chart.
Calendar, timeline, treemap
New-BGInfoTimelineThe calendar heatmap, Gantt lane and treemap chart families, sized to the panel they sit on.
Mermaid
New-BGInfoMermaidMermaid text rendered by ChartForgeX.Mermaid and re-themed for its panel.
Topology
New-BGInfoTopologyGroups, nodes and links with status and replication lag, drawn by the same TopologyChart that renders service maps in .NET.
Canvas, backdrop and QR
New-BGInfoBackgroundInfo tiles from VisualCanvas, and gradients, glows, patterns and QR modules painted with ImageComposition.
Get started
Install the module from the PowerShell Gallery and render to a file first, so you can check the layout before anything changes on a desktop.
Install the module
Install-Module PowerBGInfo
PowerBGInfo runs on Windows only. It is built for .NET Framework 4.7.2 and .NET 8 on Windows, so it works in Windows PowerShell 5.1 and in PowerShell 7.4 or later. It needs no separate helper modules; run the same command again to update.
Render a preview to a file
New-BGInfo -Theme Aurora -ThemeMode Dark -Target File {
New-BGInfoValue -BuiltinValue HostName -Name 'Computer'
New-BGInfoValue -BuiltinValue FullUserName -Name 'User'
New-BGInfoValue -BuiltinValue OSName -Name 'Windows'
New-BGInfoValue -BuiltinValue IPv4Address -Name 'IP address'
New-BGInfoValue -BuiltinValue Uptime -Name 'Up for'
New-BGInfoChart -Region Right -Kind Gauge -Metric MemoryPercent `
-Title 'Memory used' -Maximum 100 -ValueSuffix ' %'
} -ConfigurationDirectory C:\Temp\PowerBGInfo -OutputFileName preview.png
-Target File writes preview.png and leaves the desktop alone. The built-in values read the machine the script runs on, and -Metric MemoryPercent reads the memory in use at that moment, so every machine gets its own wallpaper from the same script.
Set it as the wallpaper
New-BGInfo -Theme Aurora {
New-BGInfoValue -BuiltinValue HostName -Name 'Computer'
New-BGInfoValue -BuiltinValue OSName -Name 'Windows'
} -ConfigurationDirectory C:\ProgramData\PowerBGInfo -WallpaperFit Fill
Without -Target, New-BGInfo sets the wallpaper for the account that runs it. -AllUsers stages it for every profile, and -Target LogonScreen or -Target Both sets the lock screen too; those need an elevated session. One run applies one image; run it again to refresh the values.
PowerShell GallerySource and READMEChartForgeX in PowerShell
Run it in production
PowerBGInfo has no refresh schedule of its own: each run renders the image, applies it once and exits. To keep the values current, run it again at sign-in, at unlock or on a timer, from the tool that already manages the machines.
| Route | When to use it | Guide |
|---|---|---|
| Task Scheduler | Standalone machines and labs, or any fleet where another tool runs the registration script once. The most direct way to refresh at sign-in, at unlock and on a timer, in the user's own session. | Task Scheduler guide |
| Group Policy | Active Directory domain-joined PCs and session hosts. A Group Policy Preferences task runs it for each user, and the same GPO can carry the execution policy and, on Enterprise and Education, the lock screen policy. | Group Policy guide |
| Intune | Microsoft Entra joined or hybrid joined devices. A Win32 app installs the module and the script and registers the per-user task; platform scripts, Remediations and the Personalization CSP cover the cases around it. | Intune guide |
| Configuration Manager | Devices managed by Configuration Manager, including co-managed devices where the apps workload stays with it. An application installs the module and registers the same task. | Configuration Manager guide |
| Remote Desktop, AVD, Windows 365 | Session hosts and AVD multi-session run the task in every session, with a per-user output folder and a fresh render on reconnect; LoggedOnUser names the user of the session being rendered. AVD personal desktops and Cloud PCs work like a physical PC. | Remote Desktop, AVD, Windows 365 guide |
What needs admin
Setting your own wallpaper and -Target File need no elevation. -AllUsers, -Target LogonScreen and -Target Both do, and so do installing the module for all users and registering a task for every user or for SYSTEM, once per machine. Run the wallpaper task itself as a limited user.
Signing
Evotec signs its module releases with Authenticode. Under the AllSigned execution policy, add the publisher's certificate to Trusted Publishers so an unattended task never stops at a prompt, and sign your own wallpaper script with your code-signing certificate.
Every monitor
Get-BGInfoMonitor returns each monitor's index, device name, primary flag, bounds and scale (-Primary returns only the main one), needs no elevation and works under Constrained Language Mode. Loop over it, pass -MonitorIndex and give each monitor its own -OutputFileName, as Examples/Run.BGInfo.PerMonitor.ps1 does. PowerBGInfo never draws on its own earlier output, so repeated runs do not stack the text.
Keeping logon fast
Prefer a scheduled task to a synchronous logon script, so the desktop never waits. Time-box each data source at 5 to 15 seconds; Get-BGInfoSystemInfo already limits each of its queries to five. Give the task an execution time limit so a stuck run cannot pile up.














































