# Hands-on verification of RetroBar on a real Windows machine for retrobar.dev. # Runs on GitHub Actions Windows runners (see .github/workflows/verify.yml): # windows-11-arm = Windows 11 client (Arm64) # windows-2025 = Windows Server 2025 (build 26100, the Windows 11 24H2 shell) # windows-2022 = Windows Server 2022 (the Windows 10 21H2 shell) # Downloads the official release, records hashes and signatures, installs it with # screenshots of every installer page, then captures every built-in theme, the # Properties window, settings, custom-theme tests, measurements and uninstall # leftovers. Everything goes to verify//. $OutName = if ($env:VERIFY_OUT) { $env:VERIFY_OUT } else { 'results' } . (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'lib.ps1') $Version = if ($env:RB_VERSION) { $env:RB_VERSION } else { 'v1.22.122' } $Quick = $env:RB_QUICK -eq '1' $shotsDir = Join-Path $out 'shots'; New-Item -ItemType Directory -Force -Path $shotsDir | Out-Null $dl = Join-Path $env:TEMP 'rb-download'; New-Item -ItemType Directory -Force -Path $dl | Out-Null $appData = Join-Path $env:LOCALAPPDATA 'RetroBar' $settingsFile = Join-Path $appData 'settings.json' $uninstKey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{574527FE-00A4-4F85-92AD-B4B8B4077D73}_is1' $results = [ordered]@{ version = $Version; started = (Get-Date).ToString('o') } # RB_ONLY: a comma-separated list of extra steps to run after the essential ones (machine, download, # install), for re-running one part without the whole 90-minute run $Only = if ($env:RB_ONLY) { @('machine', 'download', 'install') + ($env:RB_ONLY -split ',' | ForEach-Object { $_.Trim() }) } else { $null } function Step($name, [scriptblock]$body) { if ($Only -and $Only -notcontains $name) { return } Log "== $name" try { & $body } catch { Log "STEP FAILED ($name): $_"; $results["error_$name"] = "$_" } Save 'results.json' $results } function ShotS($name) { Shot "shots/$name" } # screenshots live in shots/ # ── 1. Machine ─────────────────────────────────────────────────────────── Step 'machine' { $os = Get-CimInstance Win32_OperatingSystem $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' $facts.os = $os.Caption $facts.build = "$($cv.CurrentBuild).$($cv.UBR)" $facts.displayVersion = $cv.DisplayVersion $facts.arch = $env:PROCESSOR_ARCHITECTURE $facts.cpu = (Get-CimInstance Win32_Processor | Select-Object -First 1).Name $facts.ramGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1) $facts.uiCulture = (Get-UICulture).Name try { $facts.dotnetRuntimes = @(& dotnet --list-runtimes 2>$null) } catch { $facts.dotnetRuntimes = @() } foreach ($a in 'x64', 'arm64', 'x86') { $k = "HKLM:\SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\$a\sharedfx\Microsoft.WindowsDesktop.App" if ($a -eq 'x86') { $k = 'HKLM:\SOFTWARE\dotnet\Setup\InstalledVersions\x86\sharedfx\Microsoft.WindowsDesktop.App' } $facts["desktopRuntimes_$a"] = @((Get-Item $k -ErrorAction SilentlyContinue).Property) } Set-Screen Dismiss-Oobe Hide-Consoles $facts.screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.ToString() $facts.dpi = (Get-ItemProperty 'HKCU:\Control Panel\Desktop\WindowMetrics' -ErrorAction SilentlyContinue).AppliedDPI $tb = [N]::FindWindow('Shell_TrayWnd', $null) $facts.windowsTaskbarVisibleBefore = [N]::IsWindowVisible($tb) $facts.windowsAutoHideBefore = ((Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StuckRects3' -ErrorAction SilentlyContinue).Settings | Select-Object -Index 8) Save 'machine.json' $facts } # Plain wallpaper (Windows 95 teal) so the taskbars are easy to see. function Set-Wallpaper($r, $g, $b) { $wp = Join-Path $env:TEMP "rb-wall-$r-$g-$b.png" $sb = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds $bmp = New-Object System.Drawing.Bitmap $sb.Width, $sb.Height $gr = [System.Drawing.Graphics]::FromImage($bmp); $gr.Clear([System.Drawing.Color]::FromArgb($r, $g, $b)); $gr.Dispose() $bmp.Save($wp, [System.Drawing.Imaging.ImageFormat]::Png); $bmp.Dispose() Add-Type -TypeDefinition 'using System.Runtime.InteropServices; public static class W { [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int SystemParametersInfo(int a, int b, string c, int d); }' -ErrorAction SilentlyContinue [W]::SystemParametersInfo(20, 0, $wp, 3) | Out-Null } Step 'desktop-before' { Set-Wallpaper 0 128 128 Start-Sleep 2 ShotS 'a01-windows-taskbar-before' } # ── 2b. Windows' own taskbar settings (for the how-to pages: what Windows can do itself) ── Step 'windows-settings' { Start-Process 'ms-settings:taskbar'; Start-Sleep 7 $w = FindTopWindow 'Settings' 20 if (-not $w) { Log 'Settings window not found'; return } $hw = [IntPtr]$w.Current.NativeWindowHandle [N]::ShowWindow($hw, 3) | Out-Null; Start-Sleep 2 # maximise, so more of the page is visible Shot 'shots/w01-taskbar-settings' $hw $names = { param($el) @($el.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition) | ForEach-Object { [string]$_.Current.Name } | Where-Object { $_ } | Select-Object -Unique) } $results.windowsTaskbarSettings = & $names $w # Expand "Taskbar behaviors" and photograph it, scrolling down $tb = FindName 'Taskbar behaviors' 5 $w if ($tb) { # It may already be open (Windows 11 25H2 opens it); clicking it then would close it $state = try { $tb.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern).Current.ExpandCollapseState } catch { 'unknown' } $results.windowsTaskbarBehaviorsStateBefore = "$state" if ("$state" -eq 'Collapsed') { try { $tb.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern).Expand() } catch { Invoke-El $tb | Out-Null } } Start-Sleep 2 # Scroll with the mouse wheel over the page until "Taskbar behaviors" is near the top, then once more $wr = $w.Current.BoundingRectangle $wx = [int]($wr.X + $wr.Width * 0.6); $wy = [int]($wr.Y + $wr.Height * 0.6) for ($i = 0; $i -lt 12; $i++) { $t = FindName 'Taskbar behaviors' 2 $w if (-not $t -or $t.Current.BoundingRectangle.Y -lt ($wr.Y + 220)) { break } Wheel $wx $wy -120; Start-Sleep -Milliseconds 600 } $results.windowsTaskbarBehaviorsScrolls = $i Start-Sleep 1; Shot 'shots/w02-taskbar-behaviors' $hw -KeepCursor for ($j = 0; $j -lt 4; $j++) { Wheel $wx $wy -120; Start-Sleep -Milliseconds 500 } Start-Sleep 1; Shot 'shots/w03-taskbar-behaviors-more' $hw -KeepCursor $results.windowsTaskbarBehaviors = & $names $w } else { Log 'Taskbar behaviors not found' } try { $w.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern).Close() } catch { Get-Process SystemSettings -ErrorAction SilentlyContinue | Stop-Process -Force } Start-Sleep 2 } # ── 2. Official files: download, hashes, signatures, Defender scan ────── Step 'download' { $h = @{ 'User-Agent' = 'retrobar.dev-verify' } if ($env:GITHUB_TOKEN) { $h.Authorization = "Bearer $env:GITHUB_TOKEN" } $rel = Invoke-RestMethod "https://api.github.com/repos/dremin/RetroBar/releases/tags/$Version" -Headers $h $results.release = [ordered]@{ tag = $rel.tag_name; published = $rel.published_at; assets = @() } foreach ($a in $rel.assets) { $f = Join-Path $dl $a.name Invoke-WebRequest $a.browser_download_url -OutFile $f -UseBasicParsing $results.release.assets += [ordered]@{ name = $a.name; size = (Get-Item $f).Length; sha256 = (Get-FileHash $f -Algorithm SHA256).Hash.ToLower(); downloads = $a.download_count } Expand-Archive $f -DestinationPath (Join-Path $dl ([IO.Path]::GetFileNameWithoutExtension($a.name))) -Force } # Every file inside the archives, with hash and Authenticode status. $results.files = @(Get-ChildItem $dl -Recurse -File | Where-Object { $_.Extension -ne '.zip' } | ForEach-Object { $sig = Get-AuthenticodeSignature $_.FullName [ordered]@{ path = $_.FullName.Substring($dl.Length + 1); size = $_.Length; sha256 = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower() signature = "$($sig.Status)"; signer = $(if ($sig.SignerCertificate) { $sig.SignerCertificate.Subject } else { '' }) fileVersion = $_.VersionInfo.FileVersion; product = $_.VersionInfo.ProductVersion } } | Where-Object { $_.path -match '\.(exe|dll)$' -or $_.size -gt 0 } | Select-Object -First 400) Log ("release assets: " + (($results.release.assets | ForEach-Object { "$($_.name) $($_.sha256)" }) -join '; ')) # The installer reads the .NET version from this URL when .NET 10 is missing (installer.iss). foreach ($u in 'https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/10.0/latest.version', 'https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0/latest.version', 'https://dremin.github.io/updates/retrobar.json') { try { $r = Invoke-WebRequest $u -UseBasicParsing -TimeoutSec 20; $results["url:$u"] = "HTTP $($r.StatusCode): $($r.Content.Trim().Substring(0, [Math]::Min(200, $r.Content.Trim().Length)))" } catch { $results["url:$u"] = "FAILED: $($_.Exception.Message)" } } } Step 'defender' { $mp = Join-Path $env:ProgramFiles 'Windows Defender\MpCmdRun.exe' if (Test-Path $mp) { try { Update-MpSignature -ErrorAction Stop; Log 'Defender signatures updated' } catch { Log "signature update: $_" } $o = & $mp -Scan -ScanType 3 -File $dl -DisableRemediation 2>&1 $results.defender = [ordered]@{ exitCode = $LASTEXITCODE; output = ($o -join "`n"); engine = (Get-MpComputerStatus -ErrorAction SilentlyContinue | Select-Object AMEngineVersion, AntivirusSignatureVersion, AntivirusSignatureLastUpdated | ConvertTo-Json -Compress) } Log "Defender exit code $LASTEXITCODE" } else { $results.defender = 'MpCmdRun.exe not present' } } function Find-Exe($root) { # RetroBar.Portable.zip (1.22) holds 32-bit, 64-bit, ARM64, Win7-8\32-bit and Win7-8\64-bit folders. $all = @(Get-ChildItem $root -Recurse -Filter 'RetroBar.exe' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch 'Win7-8' }) $want = switch ($env:PROCESSOR_ARCHITECTURE) { 'ARM64' { '\\ARM64\\' } 'x86' { '\\32-bit\\' } default { '\\64-bit\\' } } $pick = $all | Where-Object { $_.FullName -match $want } | Select-Object -First 1 if (-not $pick) { $pick = $all | Select-Object -First 1 } return $pick } function RB-Windows { $ids = @(Get-Process RetroBar -ErrorAction SilentlyContinue | ForEach-Object { $_.Id }) if (-not $ids) { return @() } return @($UIA::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition) | Where-Object { $ids -contains $_.Current.ProcessId -and $_.Current.BoundingRectangle.Width -gt 0 }) } # The taskbar window: the widest-or-tallest RetroBar window touching a screen edge. function RB-Taskbar { $sb = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds $best = $null; $score = 0 foreach ($w in RB-Windows) { $r = $w.Current.BoundingRectangle $s = [Math]::Max($r.Width, $r.Height) if (($r.Width -ge $sb.Width * 0.9 -or $r.Height -ge $sb.Height * 0.9) -and $s -gt $score) { $best = $w; $score = $s } } return $best } function Wait-RB($timeout = 30) { $end = (Get-Date).AddSeconds($timeout) while ((Get-Date) -lt $end) { $t = RB-Taskbar; if ($t) { return $t }; Start-Sleep -Milliseconds 400 } $w = @(RB-Windows) if (-not $script:LoggedNoTaskbar) { $script:LoggedNoTaskbar = $true; Log ("no full-width RetroBar window; windows: " + (($w | ForEach-Object { $b = $_.Current.BoundingRectangle; "$($_.Current.ClassName) $([int]$b.Width)x$([int]$b.Height)@$([int]$b.X),$([int]$b.Y)" }) -join '; ')) } return ($w | Sort-Object { $_.Current.BoundingRectangle.Width } -Descending | Select-Object -First 1) } # ── 3. Portable first run (shows the .NET prompt if .NET 10 is missing) ─ Step 'portable' { $pexe = Find-Exe (Join-Path $dl 'RetroBar.Portable') if (-not $pexe) { Log 'no portable exe'; return } $results.portableExe = $pexe.FullName.Substring($dl.Length + 1) Log "portable exe: $($results.portableExe)" $before = @(Get-TopWindows) $p = Start-Process $pexe.FullName -WorkingDirectory $pexe.DirectoryName -PassThru $tb = $null; $new = @() for ($i = 0; $i -lt 25 -and -not $tb; $i++) { Start-Sleep 1 $tb = RB-Taskbar $new = @(Get-TopWindows | Where-Object { $before -notcontains $_ }) if ($new | Where-Object { $_ -match '\.NET|RetroBar\.exe|#32770' }) { Start-Sleep 2; break } } ShotS 'b01-portable-first-run' $results.portableTopWindows = Get-TopWindows $results.portableNewWindows = $new # The text of a .NET "install the runtime" prompt, if that is what appeared $dlg = $new | Where-Object { $_ -match '#32770' } | Select-Object -First 1 if ($dlg) { $w = FindTopWindow ($dlg.Split('|')[1].Trim()) 3 if ($w) { $results.portableDialog = @($w.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition) | ForEach-Object { [string]$_.Current.Name } | Where-Object { $_ }) } } $results.portableStarted = [bool]$tb Get-Process RetroBar -ErrorAction SilentlyContinue | Stop-Process -Force # .NET prompt dialogs come from the apphost, not RetroBar.exe; close any leftover. Get-Process | Where-Object { $_.MainWindowTitle -match '\.NET|RetroBar' } | ForEach-Object { Log "closing window '$($_.MainWindowTitle)' ($($_.ProcessName))"; Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } Start-Sleep 3 } # ── 4. SmartScreen on a downloaded (Mark-of-the-Web) copy ─────────────── Step 'smartscreen' { $iexe = Get-ChildItem (Join-Path $dl 'RetroBar.Installer') -Recurse -Filter *.exe | Select-Object -First 1 if (-not $iexe) { Log 'no installer exe'; return } $results.installerExe = $iexe.Name $motw = Join-Path $env:USERPROFILE "Downloads\$($iexe.Name)" Copy-Item $iexe.FullName $motw -Force Set-Content -Path $motw -Stream Zone.Identifier -Value "[ZoneTransfer]`r`nZoneId=3`r`nHostUrl=https://github.com/dremin/RetroBar/releases/download/$Version/RetroBar.Installer.zip" Start-Process explorer.exe -ArgumentList "/select,`"$motw`"" ; Start-Sleep 3 Start-Process $motw ; Start-Sleep 7 ShotS 'b02-smartscreen' $results.smartscreenTopWindows = Get-TopWindows # Is SmartScreen for apps on at all on this machine? (runner images may turn it off) $ss = [ordered]@{} $ss.explorerSmartScreenEnabled = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer' -ErrorAction SilentlyContinue).SmartScreenEnabled $ss.policyEnableSmartScreen = (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' -ErrorAction SilentlyContinue).EnableSmartScreen $ss.appInstallControl = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows Defender\SmartScreen' -ErrorAction SilentlyContinue).ConfigureAppInstallControl $ss.smartScreenProcess = [bool](Get-Process smartscreen -ErrorAction SilentlyContinue) $ss.zoneIdentifier = [string](Get-Content -Path $motw -Stream Zone.Identifier -Raw -ErrorAction SilentlyContinue) $results.smartscreenState = $ss $more = FindVisible 'More info' 3 if ($more) { Invoke-El $more | Out-Null; Start-Sleep 2; ShotS 'b03-smartscreen-more-info' } Get-Process | Where-Object { $_.ProcessName -match 'RetroBarInstaller|smartscreen|RetroBar\.Installer' -or $_.MainWindowTitle -match 'Setup' } | Stop-Process -Force -ErrorAction SilentlyContinue Get-Process explorer | Where-Object { $_.MainWindowTitle } | ForEach-Object { [void]$_.CloseMainWindow() } Start-Sleep 2 } # ── 5. Installer, page by page ─────────────────────────────────────────── Step 'install' { $iexe = Get-ChildItem (Join-Path $dl 'RetroBar.Installer') -Recurse -Filter *.exe | Select-Object -First 1 $ilog = Join-Path $out 'installer-log.txt' $p = Start-Process $iexe.FullName -ArgumentList "/LOG=`"$ilog`"" -PassThru $pages = @(); $i = 0; $done = $false $end = (Get-Date).AddMinutes(6) $results.dotnetInstallerPages = @() while (-not $done -and (Get-Date) -lt $end -and $i -lt 30) { Start-Sleep 3 $i++ # On a PC without the .NET 10 Desktop Runtime, the RetroBar installer starts Microsoft's own # runtime installer (a separate window, often behind Setup) and waits until it is finished. $dn = $null foreach ($w in $UIA::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition)) { if ($w.Current.Name -match 'Desktop Runtime' -and $w.Current.ClassName -match 'WixStdBA') { $dn = $w; break } } if ($dn) { $dh = [IntPtr]$dn.Current.NativeWindowHandle [N]::SetForegroundWindow($dh) | Out-Null; Start-Sleep -Milliseconds 600 $dname = 'c{0:D2}-dotnet' -f $i Shot "shots/$dname" $dh $dt = @($dn.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition) | ForEach-Object { [string]$_.Current.Name } | Where-Object { $_ }) $results.dotnetInstallerPages += [ordered]@{ shot = $dname; title = [string]$dn.Current.Name; texts = $dt } Log ".NET installer: $($dn.Current.Name) :: $(($dt | Select-Object -First 8) -join ' | ')" foreach ($b in 'Install', 'Close') { $el = FindName $b 1 $dn; if ($el -and $el.Current.IsEnabled) { Invoke-El $el | Out-Null; Log ".NET installer: clicked '$b'"; break } } $end = $end.AddMinutes(1) continue } $win = $null foreach ($t in 'Select Setup Language', 'Setup - RetroBar', 'RetroBar Setup') { $win = FindTopWindow $t 2; if ($win) { break } } if (-not $win) { foreach ($w in $UIA::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition)) { if ($w.Current.Name -match 'Setup|RetroBar' -and $w.Current.ProcessId -ne $PID) { $win = $w; break } } } if (-not $win) { if ($p.HasExited) { $done = $true }; continue } $hw = [IntPtr]$win.Current.NativeWindowHandle [N]::SetForegroundWindow($hw) | Out-Null; Start-Sleep -Milliseconds 500 $name = 'c{0:D2}-installer' -f $i Shot "shots/$name" $hw $texts = @($win.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition) | ForEach-Object { $_.Current.Name } | Where-Object { $_ }) $pages += [ordered]@{ shot = $name; title = $win.Current.Name; texts = $texts } Log "installer page $i : $($win.Current.Name) :: $(($texts | Select-Object -First 8) -join ' | ')" # Accept the licence, tick "start automatically", then move on. $acc = FindName 'I accept the agreement' 1 $win; if ($acc) { Invoke-El $acc | Out-Null } foreach ($cb in $win.FindAll([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::ControlTypeProperty, [System.Windows.Automation.ControlType]::CheckBox)))) { $n = $cb.Current.Name try { $tp = $cb.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern) if ($n -match '(?i)automatically start|start .*log' -and $tp.Current.ToggleState -ne 'On') { $tp.Toggle(); Log "ticked '$n'" } } catch { } } $clicked = $false foreach ($b in 'OK', 'Next >', 'Next', 'Install', 'Finish') { $el = FindName $b 1 $win if ($el -and $el.Current.IsEnabled) { if ($b -eq 'Finish') { $done = $true } Invoke-El $el | Out-Null; $clicked = $true; Log "clicked '$b'"; break } } } $results.installerPages = $pages if (-not $p.HasExited) { $p.WaitForExit(120000) | Out-Null } if (-not (Get-ItemProperty "HKCU:\$uninstKey" -ErrorAction SilentlyContinue) -and -not (Get-ItemProperty "HKLM:\$uninstKey" -ErrorAction SilentlyContinue)) { Log 'interactive install did not finish; installing silently' $results.installMode = 'silent fallback' Start-Process $iexe.FullName -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /TASKS=`"autostart`" /LOG=`"$ilog.silent.txt`"" -Wait } else { $results.installMode = 'interactive' } $u = Get-ItemProperty "HKCU:\$uninstKey" -ErrorAction SilentlyContinue if (-not $u) { $u = Get-ItemProperty "HKLM:\$uninstKey" -ErrorAction SilentlyContinue; $results.uninstallHive = 'HKLM' } else { $results.uninstallHive = 'HKCU' } $results.uninstallEntry = $u | Select-Object DisplayName, DisplayVersion, Publisher, InstallLocation, UninstallString, InstallDate, EstimatedSize, URLInfoAbout $script:InstallDir = $u.InstallLocation.TrimEnd('\') $script:Exe = Join-Path $script:InstallDir 'RetroBar.exe' $results.installDir = $script:InstallDir $results.installedFiles = @(Get-ChildItem $script:InstallDir -Recurse -File | ForEach-Object { [ordered]@{ path = $_.FullName.Substring($script:InstallDir.Length + 1); size = $_.Length } }) $results.runValue = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -ErrorAction SilentlyContinue).RetroBar $results.startMenuShortcut = @(Get-ChildItem "$env:APPDATA\Microsoft\Windows\Start Menu\Programs", "$env:ProgramData\Microsoft\Windows\Start Menu\Programs" -Filter 'RetroBar*.lnk' -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) try { $results.dotnetRuntimesAfterInstall = @(& dotnet --list-runtimes 2>$null) } catch { } Log "installed to $script:InstallDir ; Run value: $($results.runValue)" } # ── helpers to drive RetroBar ─────────────────────────────────────────── function Stop-RB { Get-Process RetroBar -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep 2 } function Write-Settings($changes) { New-Item -ItemType Directory -Force -Path $appData | Out-Null $j = if (Test-Path $settingsFile) { Get-Content $settingsFile -Raw | ConvertFrom-Json } else { [pscustomobject]@{} } foreach ($k in $changes.Keys) { $j | Add-Member -NotePropertyName $k -NotePropertyValue $changes[$k] -Force } [IO.File]::WriteAllText($settingsFile, ($j | ConvertTo-Json -Depth 8), (New-Object Text.UTF8Encoding $false)) } $Base = [ordered]@{ Theme = 'Windows 95-98'; Edge = 3; RowCount = 1; TaskbarWidth = 1; LockTaskbar = $true; ShowQuickLaunch = $true; ShowClock = $true; ShowClockSeconds = $false ShowDesktopButton = $false; CollapseNotifyIcons = $false; ShowInputLanguage = $false; TaskbarScale = 1.0; AutoHide = $false; ShowTaskThumbnails = $false; CheckForUpdates = $false } function Start-RB($changes = @{}, $wait = 4) { Stop-RB $all = [ordered]@{}; foreach ($k in $Base.Keys) { $all[$k] = $Base[$k] }; foreach ($k in $changes.Keys) { $all[$k] = $changes[$k] } Write-Settings $all Start-Process $script:Exe -WorkingDirectory $script:InstallDir | Out-Null $t = Wait-RB 40 Start-Sleep $wait if ($script:Note) { try { [N]::SetForegroundWindow($script:Note.MainWindowHandle) | Out-Null } catch { } } Start-Sleep -Milliseconds 600 return $t } function TB-Rect { $t = RB-Taskbar; if ($t) { return $t.Current.BoundingRectangle } } # Right-click the clock: the notification-area menu (Adjust Date/Time, Customize Notifications, # Task Manager, Lock the Taskbar, Properties, Exit RetroBar). function TrayMenu { $t = RB-Taskbar; if (-not $t) { return $false } $clk = $t.FindFirst([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::ClassNameProperty, 'Clock'))) if (-not $clk) { return $false } $b = $clk.Current.BoundingRectangle RightClick ([int]($b.X + $b.Width / 2)) ([int]($b.Y + $b.Height / 2)); Start-Sleep 2 return $true } # Right-click an empty part of the taskbar: between the last task button and the notification area. function EmptyMenu { $t = RB-Taskbar; if (-not $t) { return $false } $r = $t.Current.BoundingRectangle $clk = $t.FindFirst([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::ClassNameProperty, 'Clock'))) $limit = if ($clk) { $clk.Current.BoundingRectangle.X - 140 } else { $r.X + $r.Width * 0.75 } $right = $r.X + 60 foreach ($e in @($t.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition))) { $b = $e.Current.BoundingRectangle if ($b.Width -gt 0 -and $b.Width -lt $r.Width * 0.5 -and ($b.X + $b.Width) -lt $limit -and ($b.X + $b.Width) -gt $right) { $right = $b.X + $b.Width } } if ($right + 30 -ge $limit) { Log 'no empty space on the taskbar'; return $false } RightClick ([int](($right + $limit) / 2)) ([int]($r.Y + $r.Height / 2)); Start-Sleep 2 return $true } # Full screenshot + the taskbar strip (1x and 2x) + the UIA tree of the taskbar. function Capture($name, [switch]$Tree) { ShotS $name $png = Join-Path $shotsDir "$name.png" $r = TB-Rect if ($r) { $pad = 0 Crop-Png $png (Join-Path $shotsDir "$name-strip.png") ($r.X - $pad) ($r.Y - $pad) ($r.Width + 2 * $pad) ($r.Height + 2 * $pad) 1 if ($r.Width -gt $r.Height) { Crop-Png $png (Join-Path $shotsDir "$name-start-2x.png") $r.X $r.Y ([Math]::Min(420, $r.Width)) $r.Height 2 Crop-Png $png (Join-Path $shotsDir "$name-tray-2x.png") ($r.X + $r.Width - 300) $r.Y 300 $r.Height 2 } if ($Tree) { $t = RB-Taskbar; Dump-Tree $t "shots/$name-tree.json" | Out-Null } return [ordered]@{ x = [int]$r.X; y = [int]$r.Y; w = [int]$r.Width; h = [int]$r.Height } } return $null } # ── 6. Windows to show on the taskbar ─────────────────────────────────── Step 'apps' { $docs = Join-Path $env:USERPROFILE 'Documents' Set-Content (Join-Path $docs 'readme.txt') 'retrobar.dev test window' $script:Note = Start-Process notepad.exe -ArgumentList (Join-Path $docs 'readme.txt') -PassThru Start-Process explorer.exe -ArgumentList $env:WINDIR Start-Process control.exe Start-Sleep 5 Dismiss-Oobe; Hide-Consoles # Quick Launch: RetroBar reads shortcuts from this folder. $ql = Join-Path $env:APPDATA 'Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar' New-Item -ItemType Directory -Force -Path $ql | Out-Null $ws = New-Object -ComObject WScript.Shell foreach ($s in @(@('File Explorer', "$env:WINDIR\explorer.exe"), @('Notepad', "$env:WINDIR\System32\notepad.exe"), @('Calculator', "$env:WINDIR\System32\calc.exe"))) { $lnk = $ws.CreateShortcut((Join-Path $ql "$($s[0]).lnk")); $lnk.TargetPath = $s[1]; $lnk.Save() } $results.quickLaunchFolder = $ql $results.quickLaunchItems = @(Get-ChildItem $ql | ForEach-Object { $_.Name }) } # ── 7. First start with default settings ──────────────────────────────── Step 'first-start' { Stop-RB if (Test-Path $settingsFile) { Copy-Item $settingsFile (Join-Path $out 'settings-after-installer.json') } Remove-Item $settingsFile -ErrorAction SilentlyContinue $sw = [Diagnostics.Stopwatch]::StartNew() $p = Start-Process $script:Exe -WorkingDirectory $script:InstallDir -PassThru $t = Wait-RB 60 $results.firstStartSeconds = [math]::Round($sw.Elapsed.TotalSeconds, 2) Start-Sleep 6 if ($script:Note) { [N]::SetForegroundWindow($script:Note.MainWindowHandle) | Out-Null } Start-Sleep 1 $results.defaultTaskbarRect = Capture 'd01-default-first-start' -Tree if (Test-Path $settingsFile) { Copy-Item $settingsFile (Join-Path $out 'settings-default.json'); $results.settingsCreatedOnFirstStart = $true } else { $results.settingsCreatedOnFirstStart = $false } # Lock the Taskbar on, then off again: RetroBar then writes every setting with its default value. $results.defaultsWritten = $false foreach ($i in 1, 2) { if (TrayMenu) { $ok = Press 'Lock the Taskbar' 5; Log "lock toggle $i -> $ok"; Start-Sleep 2 } } if (Test-Path $settingsFile) { Copy-Item $settingsFile (Join-Path $out 'settings-default.json') -Force; $results.defaultsWritten = $true $results.defaultsLockTaskbar = (Get-Content $settingsFile -Raw | ConvertFrom-Json).LockTaskbar } $tb = [N]::FindWindow('Shell_TrayWnd', $null) $results.windowsTaskbarVisibleWithRetroBar = [N]::IsWindowVisible($tb) $r = New-Object N+RECT; [N]::GetWindowRect($tb, [ref]$r) | Out-Null $results.windowsTaskbarRectWithRetroBar = "$($r.Left),$($r.Top),$($r.Right),$($r.Bottom)" $results.retroBarWindows = @(RB-Windows | ForEach-Object { $b = $_.Current.BoundingRectangle; "$($_.Current.ClassName) | $($_.Current.Name) | $([int]$b.X),$([int]$b.Y) $([int]$b.Width)x$([int]$b.Height)" }) # Network connections RetroBar opened in its first seconds (update check). try { $results.tcpAtStart = @(Get-NetTCPConnection -OwningProcess $p.Id -ErrorAction SilentlyContinue | ForEach-Object { "$($_.RemoteAddress):$($_.RemotePort) $($_.State)" }) } catch { } try { $results.dnsCache = @(Get-DnsClientCache -ErrorAction SilentlyContinue | Where-Object { $_.Entry -match 'github|dremin|dotnet|microsoft' } | ForEach-Object { $_.Entry } | Select-Object -Unique) } catch { } $results.appDataFiles = @(Get-ChildItem $appData -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName.Substring($appData.Length) }) } # ── 8. Every built-in theme ───────────────────────────────────────────── $Walls = @{ classic = @(0, 128, 128); xp = @(0, 78, 152); vista = @(30, 52, 80) } function Wall-For($theme) { if ($theme -match 'XP|Watercolor|Zune|Royale|Embedded') { 'xp' } elseif ($theme -match 'Vista|Longhorn') { 'vista' } else { 'classic' } } Step 'themes' { $themes = @('System') + @(Get-ChildItem (Join-Path $script:InstallDir 'Themes') -Filter *.xaml | ForEach-Object { $_.BaseName } | Where-Object { $_ -ne 'System' }) $results.builtInThemes = $themes $results.themeShots = [ordered]@{} $lastWall = '' foreach ($th in $themes) { $slug = ($th.ToLower() -replace '[^a-z0-9]+', '-').Trim('-') $w = Wall-For $th; if ($w -ne $lastWall) { $c = $Walls[$w]; Set-Wallpaper $c[0] $c[1] $c[2]; $lastWall = $w } Start-RB @{ Theme = $th } 4 | Out-Null $rec = [ordered]@{ locked = (Capture "t-$slug-bottom" -Tree) } if (-not $Quick) { # Hover a task button and press the Start button (hover/pressed states) $sbtn = $null $t = RB-Taskbar if ($t) { $sbtn = $t.FindFirst([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::AutomationIdProperty, 'StartButton'))) } if ($sbtn) { $br = $sbtn.Current.BoundingRectangle Click ([int]($br.X + $br.Width / 2)) ([int]($br.Y + $br.Height / 2)); Start-Sleep 2 Shot "shots/t-$slug-start-pressed" -KeepCursor Key @(0x1B); Start-Sleep 1 } Start-RB @{ Theme = $th; LockTaskbar = $false } 3 | Out-Null $rec.unlocked = Capture "t-$slug-unlocked" } $results.themeShots[$th] = $rec Log "theme $th done" } } # ── 9. Edges, rows, size, scale (a representative theme per era) ──────── Step 'layout' { $results.layoutShots = [ordered]@{} foreach ($th in 'Windows 95-98', 'Windows XP Blue', 'Windows Vista Aero') { $slug = ($th.ToLower() -replace '[^a-z0-9]+', '-').Trim('-') $c = $Walls[(Wall-For $th)]; Set-Wallpaper $c[0] $c[1] $c[2] foreach ($e in @(@(0, 'left'), @(1, 'top'), @(2, 'right'))) { Start-RB @{ Theme = $th; Edge = $e[0] } 4 | Out-Null $results.layoutShots["$th/$($e[1])"] = Capture "l-$slug-$($e[1])" } Start-RB @{ Theme = $th; RowCount = 3 } 4 | Out-Null $results.layoutShots["$th/3-rows"] = Capture "l-$slug-3-rows" Start-RB @{ Theme = $th; Edge = 0; TaskbarWidth = 3 } 4 | Out-Null $results.layoutShots["$th/left-wide"] = Capture "l-$slug-left-wide" Start-RB @{ Theme = $th; TaskbarScale = 1.5 } 4 | Out-Null $results.layoutShots["$th/scale-150"] = Capture "l-$slug-scale-150" } } # ── 10. Features (one shot per visible option) ────────────────────────── Step 'features' { $c = $Walls.xp; Set-Wallpaper $c[0] $c[1] $c[2] $feat = [ordered]@{ 'clock-seconds' = @{ Theme = 'Windows XP Blue'; ShowClockSeconds = $true } 'show-desktop-button' = @{ Theme = 'Windows XP Blue'; ShowDesktopButton = $true } 'tray-collapsed' = @{ Theme = 'Windows XP Blue'; CollapseNotifyIcons = $true } 'input-language' = @{ Theme = 'Windows XP Blue'; ShowInputLanguage = $true } 'no-quick-launch' = @{ Theme = 'Windows XP Blue'; ShowQuickLaunch = $false } 'no-clock' = @{ Theme = 'Windows XP Blue'; ShowClock = $false } 'auto-hide' = @{ Theme = 'Windows XP Blue'; AutoHide = $true } 'blur-off-vista' = @{ Theme = 'Windows Vista Aero'; AllowBlurBehind = $false } 'vista-thumbnails' = @{ Theme = 'Windows Vista Aero'; ShowTaskThumbnails = $true } } $results.featureShots = [ordered]@{} foreach ($k in $feat.Keys) { Start-RB $feat[$k] 4 | Out-Null if ($k -eq 'vista-thumbnails') { $t = RB-Taskbar $btn = @($t.FindAll([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::AutomationIdProperty, 'AppButton')))) | Select-Object -First 1 if ($btn) { $br = $btn.Current.BoundingRectangle; Hover ([int]($br.X + $br.Width / 2)) ([int]($br.Y + $br.Height / 2)); Start-Sleep 3; Shot "shots/f-$k" -KeepCursor; $results.featureShots[$k] = 'hovered'; continue } } $results.featureShots[$k] = Capture "f-$k" } # Context menus: taskbar, task button, clock click (Notification Center on Windows 10/11) Start-RB @{ Theme = 'Windows XP Blue'; ShowEndTaskButton = $true } 4 | Out-Null $r = TB-Rect if ($r) { if (EmptyMenu) { Shot 'shots/f-taskbar-context-menu' -KeepCursor; $results.taskbarMenuHasProperties = [bool](FindVisible 'Properties' 3); Key @(0x1B); Start-Sleep 1 } if (TrayMenu) { Shot 'shots/f-tray-context-menu' -KeepCursor; Key @(0x1B); Start-Sleep 1 } $t = RB-Taskbar $btn = @($t.FindAll([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::AutomationIdProperty, 'AppButton')))) | Select-Object -First 1 if ($btn) { $br = $btn.Current.BoundingRectangle; RightClick ([int]($br.X + $br.Width / 2)) ([int]($br.Y + $br.Height / 2)); Start-Sleep 2; Shot 'shots/f-task-context-menu' -KeepCursor; Key @(0x1B); Start-Sleep 1 } $clk = $t.FindFirst([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::ClassNameProperty, 'Clock'))) $results.clockElement = [bool]$clk } } # ── 11. Properties window (all tabs) in three themes ──────────────────── Step 'properties' { $results.propertiesTabs = [ordered]@{} foreach ($th in 'Windows 95-98', 'Windows XP Blue', 'Windows Vista Aero') { $slug = ($th.ToLower() -replace '[^a-z0-9]+', '-').Trim('-') # Defaults only (plus the theme), so the screenshots show RetroBar's default choices Stop-RB; Remove-Item $settingsFile -ErrorAction SilentlyContinue [IO.File]::WriteAllText($settingsFile, (@{ Theme = $th } | ConvertTo-Json), (New-Object Text.UTF8Encoding $false)) Start-Process $script:Exe -WorkingDirectory $script:InstallDir | Out-Null; Wait-RB 40 | Out-Null; Start-Sleep 4 if (-not (TrayMenu) -or -not (Press 'Properties' 4)) { Log "Properties not opened for $th"; Key @(0x1B); continue } $win = FindTopWindow 'RetroBar Properties' 15 if (-not $win) { Log 'Properties window not found'; continue } Start-Sleep 2 $hw = [IntPtr]$win.Current.NativeWindowHandle $tabs = @($win.FindAll([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::ControlTypeProperty, [System.Windows.Automation.ControlType]::TabItem)))) $results.propertiesTabs[$th] = @($tabs | ForEach-Object { $_.Current.Name }) $n = 0 foreach ($tab in $tabs) { $n++ try { $tab.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Select() } catch { Invoke-El $tab | Out-Null } Start-Sleep 1 Shot "shots/p-$slug-tab$n" $hw if ($th -eq 'Windows 95-98') { Dump-Tree $win "shots/p-$slug-tab$n-tree.json" | Out-Null } } try { $win.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern).Close() } catch { Key @(0x1B) } Start-Sleep 2 } } # ── 12. Custom themes: where they load from and what breaks ───────────── # Error lines RetroBar logged since $since, as plain strings, plus any error dialog on screen. function Theme-Log($since) { $lines = @(Get-ChildItem (Join-Path $appData 'Logs') -File -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge $since.AddSeconds(-2) } | ForEach-Object { $fs = [IO.File]::Open($_.FullName, 'Open', 'Read', 'ReadWrite'); try { (New-Object IO.StreamReader($fs)).ReadToEnd() -split "`r?`n" } finally { $fs.Dispose() } } | Where-Object { $_ -match '(?i)error|exception|warn|dictionar|theme|could not' } | Select-Object -First 30) $dlg = @(Get-TopWindows | Where-Object { $_ -match '#32770' -and $_ -match 'RetroBar' }) return [ordered]@{ lines = @($lines | ForEach-Object { [string]$_ }); dialogs = $dlg } } Step 'custom-themes' { $td = Join-Path $appData 'Themes'; New-Item -ItemType Directory -Force -Path $td | Out-Null $builtin = Join-Path $script:InstallDir 'Themes' $xpBlue = [IO.File]::ReadAllText((Join-Path $builtin 'Windows XP Blue.xaml')) $header = '' $tests = [ordered]@{ # A: inherits a built-in theme by relative file name, as the built-in themes do 'Test A Merge Relative' = "$header" # B: inherits with an absolute path to the installed theme 'Test B Merge Absolute' = "$header" # C: an unmodified copy of a built-in theme (images use ../Resources/) 'Test C Copy Of XP Blue' = $xpBlue # D: the same copy with image paths made absolute 'Test D Copy Absolute Images' = ($xpBlue -replace '\.\./Resources/', (($script:InstallDir -replace '\\', '/') + '/Resources/')) # E: a simple self-contained theme (no images, no inheritance) 'Test E Simple Colors' = "$header" # I: inherits the installed theme with a path relative to %LOCALAPPDATA%\RetroBar\Themes (no user name in it) 'Test I Merge Relative To Programs' = "$header" # J: the full copy with its images pointed at the installed Resources folder the same way 'Test J Copy Images Relative To Programs' = ($xpBlue -replace '\.\./Resources/', '../../Programs/RetroBar/Resources/') # F: broken XAML 'Test F Broken' = "$header" } $results.customThemes = [ordered]@{} $c = $Walls.xp; Set-Wallpaper $c[0] $c[1] $c[2] foreach ($k in $tests.Keys) { [IO.File]::WriteAllText((Join-Path $td "$k.xaml"), $tests[$k], (New-Object Text.UTF8Encoding $false)) $slug = ($k.ToLower() -replace '[^a-z0-9]+', '-').Trim('-') Stop-RB; Remove-Item (Join-Path $appData 'Logs') -Recurse -Force -ErrorAction SilentlyContinue; $since = Get-Date $t = Start-RB @{ Theme = $k; DebugLogging = $true } 5 $rect = Capture "x-$slug" $logs = Theme-Log $since $results.customThemes[$k] = [ordered]@{ running = [bool](Get-Process RetroBar -ErrorAction SilentlyContinue); taskbar = $rect; log = @($logs) } Log "custom theme $k : running=$($results.customThemes[$k].running) log lines=$(@($logs).Count)" } # Is the custom theme listed in Properties? (themes found in %LOCALAPPDATA%\RetroBar\Themes) $results.customThemeFolderListing = @(Get-ChildItem $td | ForEach-Object { $_.Name }) # G: the unmodified copy placed in the program's own Themes folder (next to the built-in ones) $g = Join-Path $builtin 'Test G Copy In Program Folder.xaml' [IO.File]::WriteAllText($g, $xpBlue, (New-Object Text.UTF8Encoding $false)) # H: the unmodified copy in AppData, with the program's Resources folder copied to %LOCALAPPDATA%\RetroBar\Resources $res = Join-Path $appData 'Resources' Copy-Item (Join-Path $script:InstallDir 'Resources') $res -Recurse -Force [IO.File]::WriteAllText((Join-Path $td 'Test H Copy With Resources.xaml'), $xpBlue, (New-Object Text.UTF8Encoding $false)) $results.resourcesFolderFiles = @(Get-ChildItem $res -File | ForEach-Object { $_.Name }) foreach ($k in 'Test G Copy In Program Folder', 'Test H Copy With Resources') { $slug = ($k.ToLower() -replace '[^a-z0-9]+', '-').Trim('-') Stop-RB; Remove-Item (Join-Path $appData 'Logs') -Recurse -Force -ErrorAction SilentlyContinue; $since = Get-Date Start-RB @{ Theme = $k; DebugLogging = $true } 5 | Out-Null $rect = Capture "x-$slug" $logs = Theme-Log $since $results.customThemes[$k] = [ordered]@{ running = [bool](Get-Process RetroBar -ErrorAction SilentlyContinue); taskbar = $rect; log = @($logs) } Log "custom theme $k : running=$($results.customThemes[$k].running) log lines=$(@($logs).Count)" } Remove-Item $g, $res -Recurse -Force -ErrorAction SilentlyContinue # Themes exported by retrobar.dev's Theme Studio (verify/studio-samples), as a user would save them $results.studioThemes = [ordered]@{} foreach ($f in Get-ChildItem (Join-Path $PSScriptRoot 'studio-samples') -Filter *.xaml -ErrorAction SilentlyContinue) { Copy-Item $f.FullName (Join-Path $td $f.Name) -Force $k = $f.BaseName; $slug = ($k.ToLower() -replace '[^a-z0-9]+', '-').Trim('-') Stop-RB; Remove-Item (Join-Path $appData 'Logs') -Recurse -Force -ErrorAction SilentlyContinue; $since = Get-Date Start-RB @{ Theme = $k; DebugLogging = $true } 5 | Out-Null $rect = Capture "x-$slug" $results.studioThemes[$k] = [ordered]@{ running = [bool](Get-Process RetroBar -ErrorAction SilentlyContinue); taskbar = $rect; log = (Theme-Log $since) } Log "studio theme $k : running=$($results.studioThemes[$k].running)" Remove-Item (Join-Path $td $f.Name) -Force -ErrorAction SilentlyContinue } $results.logFolder = (Join-Path $appData 'Logs') } # ── 12b. Community themes, installed exactly as their READMEs say ────── # Pinned commits, so the results describe exactly the files we link to. Step 'community-themes' { $repos = @( @{ owner = 'DimonByte'; repo = 'RetroBar_Windows7_Theme'; sha = '3152bb83bd8995d6d6d2cc73c9247e11c0e56897' }, @{ owner = 'mvf'; repo = 'RetroBar-Themes'; sha = 'e9467c470a2459b28784261bbefdfb223474d91b' } ) $results.communityThemes = [ordered]@{} $c = $Walls.vista; Set-Wallpaper $c[0] $c[1] $c[2] foreach ($r in $repos) { $zip = Join-Path $env:TEMP "$($r.repo).zip"; $dir = Join-Path $env:TEMP "cm-$($r.repo)" Invoke-WebRequest "https://codeload.github.com/$($r.owner)/$($r.repo)/zip/$($r.sha)" -OutFile $zip -UseBasicParsing Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue; Expand-Archive $zip $dir -Force $root = Get-ChildItem $dir -Directory | Select-Object -First 1 # "Copy the Themes and Resources folders to %localappdata%\RetroBar" (both READMEs) $added = @() foreach ($sub in 'Themes', 'Resources') { $src = Join-Path $root.FullName $sub; if (-not (Test-Path $src)) { continue } $dst = Join-Path $appData $sub; New-Item -ItemType Directory -Force -Path $dst | Out-Null foreach ($f in Get-ChildItem $src -File) { Copy-Item $f.FullName $dst -Force; $added += (Join-Path $dst $f.Name) } } foreach ($t in Get-ChildItem (Join-Path $root.FullName 'Themes') -Filter *.xaml) { $name = $t.BaseName $variants = [ordered]@{ '' = @{} } if ($r.repo -match 'Windows7') { $variants['-collapsed'] = @{ CollapseNotifyIcons = $true } } foreach ($v in $variants.Keys) { $slug = ('cm-' + ($name.ToLower() -replace '[^a-z0-9]+', '-').Trim('-') + $v) Stop-RB; Remove-Item (Join-Path $appData 'Logs') -Recurse -Force -ErrorAction SilentlyContinue; $since = Get-Date $chg = @{ Theme = $name; DebugLogging = $true }; foreach ($k in $variants[$v].Keys) { $chg[$k] = $variants[$v][$k] } Start-RB $chg 5 | Out-Null $rect = Capture $slug $results.communityThemes["$name$v"] = [ordered]@{ repo = "$($r.owner)/$($r.repo)"; commit = $r.sha; running = [bool](Get-Process RetroBar -ErrorAction SilentlyContinue); taskbar = $rect; log = (Theme-Log $since) } Log "community theme $name$v : running=$($results.communityThemes["$name$v"].running) taskbar=$([bool]$rect)" } } Stop-RB $added | ForEach-Object { Remove-Item $_ -Force -ErrorAction SilentlyContinue } } } # ── 13. Our theme-switch script (answers RetroBar issue #958) ─────────── Step 'switch-script' { Start-RB @{ Theme = 'Windows 95-98' } 3 | Out-Null $script = Join-Path $root 'Set-RetroBarTheme.ps1' $o1 = & powershell -NoProfile -ExecutionPolicy Bypass -File $script -List 2>&1 $results.switchScriptList = @($o1 | ForEach-Object { "$_" }) $o2 = & powershell -NoProfile -ExecutionPolicy Bypass -File $script -Theme 'Windows XP Silver' 2>&1 $results.switchScriptOutput = @($o2 | ForEach-Object { "$_" }) $t = Wait-RB 30; Start-Sleep 4 $results.switchScriptThemeAfter = (Get-Content $settingsFile -Raw | ConvertFrom-Json).Theme Capture 'g01-switch-script-xp-silver' | Out-Null $o3 = & powershell -NoProfile -ExecutionPolicy Bypass -Command "& '$script' -Theme 'Windows 2000' -Set @{ ShowClockSeconds = `$true; Edge = 1 }" 2>&1 $results.switchScriptOutput2 = @($o3 | ForEach-Object { "$_" }) $t = Wait-RB 30; Start-Sleep 4 Capture 'g02-switch-script-2000-top-seconds' | Out-Null $results.closeMainWindowWorks = $null $p = Get-Process RetroBar -ErrorAction SilentlyContinue | Select-Object -First 1 if ($p) { $results.closeMainWindowReturned = $p.CloseMainWindow(); $results.closeMainWindowWorks = $p.WaitForExit(6000) } } # ── 14. Start button opens the Windows Start menu ─────────────────────── Step 'start-menu' { $c = $Walls.xp; Set-Wallpaper $c[0] $c[1] $c[2] Start-RB @{ Theme = 'Windows XP Blue' } 4 | Out-Null $t = RB-Taskbar $sbtn = $t.FindFirst([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::AutomationIdProperty, 'StartButton'))) if ($sbtn) { $br = $sbtn.Current.BoundingRectangle Click ([int]($br.X + $br.Width / 2)) ([int]($br.Y + $br.Height / 2)); Start-Sleep 3 Shot 'shots/s01-start-menu-from-retrobar' -KeepCursor $results.topWindowsWithStartOpen = Get-TopWindows Key @(0x1B); Start-Sleep 1 } Key @(0x5B); Start-Sleep 3 Shot 'shots/s02-windows-key' Key @(0x1B); Start-Sleep 1 # Windows' own shortcuts for parts of the Windows 11 taskbar that RetroBar does not have $results.shortcutsWithRetroBar = [ordered]@{} foreach ($sc in @(@('s03-win-s', @(0x5B, 0x53)), @('s04-win-tab', @(0x5B, 0x09)), @('s05-win-a', @(0x5B, 0x41)), @('s06-win-n', @(0x5B, 0x4E)))) { Key $sc[1]; Start-Sleep 3 Shot "shots/$($sc[0])" $results.shortcutsWithRetroBar[$sc[0]] = @(Get-TopWindows | Select-Object -First 6) Key @(0x1B); Start-Sleep 2 } } # ── 15. Measurements ──────────────────────────────────────────────────── Step 'measure' { $m = [ordered]@{ runs = @() } foreach ($th in 'Windows 95-98', 'Windows XP Blue', 'Windows Vista Aero') { Stop-RB $all = [ordered]@{}; foreach ($k in $Base.Keys) { $all[$k] = $Base[$k] }; $all.Theme = $th Write-Settings $all $sw = [Diagnostics.Stopwatch]::StartNew() $p = Start-Process $script:Exe -WorkingDirectory $script:InstallDir -PassThru $t = Wait-RB 60 $startS = [math]::Round($sw.Elapsed.TotalSeconds, 2) Start-Sleep 45 $p.Refresh() $cpu0 = $p.TotalProcessorTime; Start-Sleep 30; $p.Refresh(); $cpu1 = $p.TotalProcessorTime $samples = foreach ($i in 1..5) { $p.Refresh(); [math]::Round($p.WorkingSet64 / 1MB, 1); Start-Sleep 2 } $p.Refresh() $m.runs += [ordered]@{ theme = $th; secondsToTaskbar = $startS; workingSetMB = ($samples | Sort-Object)[2]; privateMB = [math]::Round($p.PrivateMemorySize64 / 1MB, 1) cpuPercentIdle30s = [math]::Round(($cpu1 - $cpu0).TotalSeconds / 30 / [Environment]::ProcessorCount * 100, 3); threads = $p.Threads.Count; handles = $p.HandleCount } } $results.measurements = $m Log ($m | ConvertTo-Json -Depth 4 -Compress) } # ── 16. winget ────────────────────────────────────────────────────────── Step 'winget' { $wg = Get-Command winget -ErrorAction SilentlyContinue if ($wg) { $results.wingetShow = @(& winget show --id dremin.RetroBar --exact --accept-source-agreements 2>&1 | ForEach-Object { "$_" }) } else { $results.wingetShow = 'winget not available on this runner' } } # ── 16b. Open-Shell + RetroBar: our three presets (retrobar.dev/start-menu/) ─ Step 'openshell' { $h = @{ 'User-Agent' = 'retrobar.dev-verify' }; if ($env:GITHUB_TOKEN) { $h.Authorization = "Bearer $env:GITHUB_TOKEN" } $rel = Invoke-RestMethod 'https://api.github.com/repos/Open-Shell/Open-Shell-Menu/releases/latest' -Headers $h $asset = $rel.assets | Where-Object { $_.name -match '^OpenShellSetup_.*\.exe$' } | Select-Object -First 1 $setup = Join-Path $dl $asset.name Invoke-WebRequest $asset.browser_download_url -OutFile $setup -UseBasicParsing $results.openShell = [ordered]@{ version = $rel.tag_name; setup = $asset.name; sha256 = (Get-FileHash $setup -Algorithm SHA256).Hash.ToLower(); signature = "$((Get-AuthenticodeSignature $setup).Status)" } $p = Start-Process $setup -ArgumentList '/qn ADDLOCAL=StartMenu' -PassThru -Wait $results.openShell.installExit = $p.ExitCode $sm = @("$env:ProgramFiles\Open-Shell\StartMenu.exe", "${env:ProgramFiles(x86)}\Open-Shell\StartMenu.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1 $results.openShell.startMenuExe = $sm if (-not $sm) { Log 'Open-Shell StartMenu.exe not found'; return } $work = 'C:\rbtest'; New-Item -ItemType Directory -Force -Path $work | Out-Null $presets = [ordered]@{ 'retrobar-classic' = 'Windows 95-98'; 'retrobar-xp' = 'Windows XP Blue'; 'retrobar-vista' = 'Windows Vista Aero' } $results.openShell.presets = [ordered]@{} foreach ($k in $presets.Keys) { Copy-Item (Join-Path $repo "downloads\openshell\$k.xml") (Join-Path $work "$k.xml") -Force $imp = Start-Process $sm -ArgumentList "-xml $work\$k.xml" -PassThru -Wait Start-Sleep 2 $bk = Join-Path $work "$k-backup.xml" Start-Process $sm -ArgumentList "-backup $bk" -Wait if (-not (Get-Process StartMenu -ErrorAction SilentlyContinue)) { Start-Process $sm; Start-Sleep 3 } $th = $presets[$k]; $c = $Walls[(Wall-For $th)]; Set-Wallpaper $c[0] $c[1] $c[2] Start-RB @{ Theme = $th } 4 | Out-Null $t = RB-Taskbar $sbtn = if ($t) { $t.FindFirst([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::AutomationIdProperty, 'StartButton'))) } if ($sbtn) { $br = $sbtn.Current.BoundingRectangle; Click ([int]($br.X + $br.Width / 2)) ([int]($br.Y + $br.Height / 2)); Start-Sleep 3; Shot "shots/o-$k-start" -KeepCursor; $tw = Get-TopWindows; Key @(0x1B); Start-Sleep 1 } Key @(0x5B); Start-Sleep 3; Shot "shots/o-$k-winkey"; Key @(0x1B); Start-Sleep 1 $backup = if (Test-Path $bk) { [IO.File]::ReadAllText($bk) } else { '' } $results.openShell.presets[$k] = [ordered]@{ importExit = $imp.ExitCode; backupHas = @(([regex]::Matches($backup, '<(MenuStyle|SkinC1|SkinC2|SkinW7|WinKey|AlignToWorkArea|EnableStartButton) value="[^"]*"')) | ForEach-Object { $_.Value }); topWindows = $tw } Copy-Item $bk (Join-Path $out "openshell-$k-backup.xml") -ErrorAction SilentlyContinue } } # ── 16c. Retro Makeover scripts (retrobar.dev/tools/makeover/), then their undo script ─ # The samples are the tool's own output for Windows 95, XP and 7. Open-Shell is removed first, # so the first script tests the download, the SHA-256 check and the silent install. Step 'makeover' { $results.makeover = [ordered]@{ samples = [ordered]@{} } # Only Windows Installer entries (key = product code); msiexec /x with anything else shows a help window and waits $key = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' $entries = @(Get-ItemProperty $key -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match '^Open-Shell' }) $results.makeover.openShellEntries = @($entries | ForEach-Object { "$($_.PSChildName) | $($_.DisplayName) | $($_.UninstallString)" }) $results.makeover.openShellRemoved = @() foreach ($os in $entries | Where-Object { $_.PSChildName -match '^\{[0-9A-Fa-f-]{36}\}$' }) { Get-Process StartMenu -ErrorAction SilentlyContinue | Stop-Process -Force Log "removing $($os.DisplayName) $($os.PSChildName)" $p = Start-Process msiexec.exe -ArgumentList "/x $($os.PSChildName) /qn /norestart" -PassThru; $null = $p.Handle $done = $p.WaitForExit(240000); if (-not $done) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue; Log 'msiexec did not finish in 4 minutes' } $results.makeover.openShellRemoved += [ordered]@{ product = $os.DisplayName; code = $os.PSChildName; exit = $(if ($done) { $p.ExitCode } else { 'timeout' }) } } # The community-themes step installed the Windows 7 theme already; remove it so the Windows 7 script's own download is what we test $w7 = @(Get-ChildItem (Join-Path $appData 'Themes') -Filter 'Windows 7 Aero*.xaml' -ErrorAction SilentlyContinue) $w7 | Remove-Item -Force $results.makeover.win7ThemeFilesRemovedFirst = $w7.Count $results.makeover.openShellBefore = [bool](@("$env:ProgramFiles\Open-Shell\StartMenu.exe", "${env:ProgramFiles(x86)}\Open-Shell\StartMenu.exe") | Where-Object { Test-Path $_ }) Start-RB @{ Theme = 'Windows 95-98' } 2 | Out-Null $keys = 'Theme', 'ShowQuickLaunch', 'CollapseNotifyIcons', 'ShowTaskThumbnails', 'ClockClickAction' $snap = { $j = if (Test-Path $settingsFile) { Get-Content $settingsFile -Raw | ConvertFrom-Json }; [ordered]@{ settings = ($keys | ForEach-Object { "$_=$(if ($j) { $j.$_ })" }) -join ';'; background = (Get-ItemProperty 'HKCU:\Control Panel\Colors').Background; wallpaper = (Get-ItemProperty 'HKCU:\Control Panel\Desktop').WallPaper } } $before = & $snap $results.makeover.before = $before $osBackup = { param($f) $sm = @("$env:ProgramFiles\Open-Shell\StartMenu.exe", "${env:ProgramFiles(x86)}\Open-Shell\StartMenu.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1; if (-not $sm) { return @() }; Start-Process $sm -ArgumentList "-backup `"$f`"" -Wait; if (Test-Path $f) { @([regex]::Matches([IO.File]::ReadAllText($f), '<(MenuStyle|SkinC1|SkinC2|SkinW7|WinKey|AlignToWorkArea|EnableStartButton) value="[^"]*"') | ForEach-Object { $_.Value }) } else { @() } } $run = { param($file, $extra) $o = Join-Path $env:TEMP "mk-out-$([IO.Path]::GetFileNameWithoutExtension($file)).txt" $p = Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$file`" $extra" -RedirectStandardOutput $o -RedirectStandardError "$o.err" -PassThru -WindowStyle Hidden $null = $p.Handle # without this, ExitCode stays empty for redirected processes $done = $p.WaitForExit(600000); if (-not $done) { Stop-Process -Id $p.Id -Force } [ordered]@{ exit = if ($done) { $p.ExitCode } else { 'timeout' }; output = @((Get-Content $o -ErrorAction SilentlyContinue) + (Get-Content "$o.err" -ErrorAction SilentlyContinue) | ForEach-Object { "$_" }) } } foreach ($f in Get-ChildItem (Join-Path $repo 'verify\makeover-samples') -Filter *.ps1 | Sort-Object Name) { $k = $f.BaseName Log "makeover sample $k" $errs = $null; [void][System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$null, [ref]$errs) $t0 = Get-Date $r = & $run $f.FullName '-NoPause' $r.parseErrors = @($errs | ForEach-Object { "$($_.Extent.StartLineNumber): $($_.Message)" }) $r.seconds = [int]((Get-Date) - $t0).TotalSeconds $t = Wait-RB 40; Start-Sleep 4 $j = if (Test-Path $settingsFile) { Get-Content $settingsFile -Raw | ConvertFrom-Json } $r.themeSetting = if ($j) { $j.Theme } $r.settings = (& $snap).settings $r.taskbar = if ($t) { $b = $t.Current.BoundingRectangle; [ordered]@{ w = [int]$b.Width; h = [int]$b.Height } } $r.startMenuRunning = [bool](Get-Process StartMenu -ErrorAction SilentlyContinue) $r.openShellBackupHas = & $osBackup (Join-Path $env:TEMP "mk-$k-openshell.xml") $r.background = (Get-ItemProperty 'HKCU:\Control Panel\Colors').Background $r.wallpaper = (Get-ItemProperty 'HKCU:\Control Panel\Desktop').WallPaper $r.undoScript = Test-Path (Join-Path $appData 'Undo-RetroMakeover.ps1') $r.log = Theme-Log $t0 $t = RB-Taskbar $sbtn = if ($t) { $t.FindFirst([System.Windows.Automation.TreeScope]::Descendants, (New-Object System.Windows.Automation.PropertyCondition($UIA::AutomationIdProperty, 'StartButton'))) } if ($sbtn) { $br = $sbtn.Current.BoundingRectangle; Click ([int]($br.X + $br.Width / 2)) ([int]($br.Y + $br.Height / 2)); Start-Sleep 3; Shot "shots/x-makeover-$k" -KeepCursor; $r.topWindows = Get-TopWindows; Key @(0x1B); Start-Sleep 1 } else { ShotS "x-makeover-$k" } $results.makeover.samples[$k] = $r Log "makeover $k : exit $($r.exit), theme '$($r.themeSetting)', background '$($r.background)', Open-Shell $($r.startMenuRunning)" } $u = Join-Path $appData 'Undo-RetroMakeover.ps1' $undo = [ordered]@{ exists = Test-Path $u } if ($undo.exists) { Copy-Item $u (Join-Path $out 'makeover-undo-script.ps1') $r = & $run $u '' $undo.exit = $r.exit; $undo.output = $r.output Wait-RB 40 | Out-Null; Start-Sleep 4 $after = & $snap $undo.after = $after $undo.settingsRestored = $after.settings -eq $before.settings $undo.desktopRestored = ($after.background -eq $before.background) -and ($after.wallpaper -eq $before.wallpaper) $undo.undoScriptRemoved = -not (Test-Path $u) $undo.backupsRemoved = -not (Test-Path (Join-Path $appData 'settings.before-makeover.json')) ShotS 'x-makeover-undo' } $results.makeover.undo = $undo } # ── 17. Exit RetroBar from its menu: does the Windows taskbar come back? ─ Step 'exit' { Set-Wallpaper 0 128 128 Start-RB @{ Theme = 'Windows 95-98' } 4 | Out-Null $r = TB-Rect RightClick ([int]($r.X + $r.Width * 0.62)) ([int]($r.Y + $r.Height / 2)); Start-Sleep 2 $ok = Press 'Exit RetroBar' 4 Start-Sleep 5 $results.exitViaMenu = $ok $results.retroBarRunningAfterExit = [bool](Get-Process RetroBar -ErrorAction SilentlyContinue) $tb = [N]::FindWindow('Shell_TrayWnd', $null) $results.windowsTaskbarVisibleAfterExit = [N]::IsWindowVisible($tb) ShotS 'e01-after-exit' } # ── 18. Uninstall, with the "delete settings?" question ───────────────── Step 'uninstall' { Stop-RB $un = Join-Path $script:InstallDir 'unins000.exe' $p = Start-Process $un -PassThru $answers = @() $end = (Get-Date).AddMinutes(3); $i = 0 while ((Get-Date) -lt $end -and $i -lt 8) { Start-Sleep 3; $i++ $win = $null foreach ($w in $UIA::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition)) { if ($w.Current.Name -match 'Uninstall|RetroBar') { $win = $w; break } } if (-not $win) { if (-not (Get-Process | Where-Object { $_.ProcessName -match '^_?iu|unins' })) { break }; continue } $hw = [IntPtr]$win.Current.NativeWindowHandle Shot ('shots/u{0:D2}-uninstall' -f $i) $hw $texts = @($win.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition) | ForEach-Object { $_.Current.Name } | Where-Object { $_ }) $answers += "$($win.Current.Name) :: $($texts -join ' | ')" # Keep settings (answer No to "delete the RetroBar user settings?") so we can see what is left. if (($texts -join ' ') -match 'user settings') { Press 'No' 2 $win | Out-Null } elseif (FindName 'Yes' 1 $win) { Press 'Yes' 2 $win | Out-Null } else { Press 'OK' 2 $win | Out-Null } } $results.uninstallDialogs = $answers Start-Sleep 5 $results.afterUninstall = [ordered]@{ installDirExists = (Test-Path $script:InstallDir) installDirLeft = @(Get-ChildItem $script:InstallDir -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) appDataLeft = @(Get-ChildItem $appData -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName.Substring($appData.Length) }) runValue = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -ErrorAction SilentlyContinue).RetroBar uninstallKey = [bool](Get-Item "HKCU:\$uninstKey" -ErrorAction SilentlyContinue) windowsTaskbarVisible = [N]::IsWindowVisible([N]::FindWindow('Shell_TrayWnd', $null)) startMenuShortcutLeft = @(Get-ChildItem "$env:APPDATA\Microsoft\Windows\Start Menu\Programs" -Filter 'RetroBar*.lnk' -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) desktopShortcutLeft = [bool](Test-Path (Join-Path ([Environment]::GetFolderPath('Desktop')) 'RetroBar.lnk')) } ShotS 'u99-after-uninstall' } $results.finished = (Get-Date).ToString('o') Save 'results.json' $results Log 'done'