#Requires -Version 5.1 # SilverMetal WinPE pre-config collector (WinForms UI). Runs in WinPE under the # ADK WinPE-NetFx + WinPE-PowerShell optional components. ASCII body only (WinPE # PowerShell 5.1 mis-parses smart quotes / em-dashes). On Finish it generates the # answer file and launches legacy Setup; on Cancel / error it exits 1 so the # wrapping Start-Collector.cmd falls back to the default autounattend.xml. try { Add-Type -AssemblyName System.Windows.Forms, System.Drawing # Validation helpers + answer-file generator live next to this script under X:\sm\. . (Join-Path $PSScriptRoot 'Test-SmInput.ps1') . (Join-Path $PSScriptRoot 'New-SmAnswerFile.ps1') # --- palette --------------------------------------------------------------- $colBg = [Drawing.Color]::FromArgb(18, 18, 22) $colPanel = [Drawing.Color]::FromArgb(28, 28, 34) $colText = [Drawing.Color]::FromArgb(232, 232, 238) $colMuted = [Drawing.Color]::FromArgb(150, 150, 162) $colAccent = [Drawing.Color]::FromArgb(96, 200, 170) $colError = [Drawing.Color]::FromArgb(236, 110, 110) $colField = [Drawing.Color]::FromArgb(40, 40, 48) $fontBase = New-Object Drawing.Font('Segoe UI', 11) $fontTitle = New-Object Drawing.Font('Segoe UI Semibold', 26, [Drawing.FontStyle]::Bold) $fontLabel = New-Object Drawing.Font('Segoe UI', 10) # --- form ------------------------------------------------------------------ $form = New-Object Windows.Forms.Form $form.Text = 'SilverMetal Setup' $form.WindowState = 'Maximized' $form.FormBorderStyle = 'None' $form.BackColor = $colBg $form.ForeColor = $colText $form.Font = $fontBase $form.KeyPreview = $true # Helpers to build consistent controls. function New-Label([string]$text, [int]$x, [int]$y, [int]$w = 220) { $l = New-Object Windows.Forms.Label $l.Text = $text; $l.AutoSize = $false $l.Location = New-Object Drawing.Point($x, $y) $l.Size = New-Object Drawing.Size($w, 24) $l.ForeColor = $colMuted; $l.Font = $fontLabel $form.Controls.Add($l); $l } function New-Field([int]$x, [int]$y, [int]$w = 360, [bool]$mask = $false) { $t = New-Object Windows.Forms.TextBox $t.Location = New-Object Drawing.Point($x, $y) $t.Size = New-Object Drawing.Size($w, 28) $t.BackColor = $colField; $t.ForeColor = $colText $t.BorderStyle = 'FixedSingle'; $t.Font = $fontBase if ($mask) { $t.UseSystemPasswordChar = $true } $form.Controls.Add($t); $t } # Title + subtitle (left rail). $title = New-Object Windows.Forms.Label $title.Text = 'SilverMetal' $title.Font = $fontTitle; $title.ForeColor = $colAccent $title.AutoSize = $true $title.Location = New-Object Drawing.Point(80, 70) $form.Controls.Add($title) $subtitle = New-Object Windows.Forms.Label $subtitle.Text = 'Pre-install configuration. Set your account, machine and security options before Windows installs.' $subtitle.Font = $fontLabel; $subtitle.ForeColor = $colMuted $subtitle.AutoSize = $false $subtitle.Location = New-Object Drawing.Point(82, 120) $subtitle.Size = New-Object Drawing.Size(640, 24) $form.Controls.Add($subtitle) # --- left column fields ---------------------------------------------------- $colX = 80; $fldX = 80; $y = 180; $rowH = 64 New-Label 'Display name' $colX $y | Out-Null $txtDisplay = New-Field $fldX ($y + 26) $y += $rowH New-Label 'Username' $colX $y | Out-Null $txtUser = New-Field $fldX ($y + 26) $y += $rowH New-Label 'Password' $colX $y | Out-Null $txtPass = New-Field $fldX ($y + 26) 360 $true $y += $rowH New-Label 'Confirm password' $colX $y | Out-Null $txtPassC = New-Field $fldX ($y + 26) 360 $true $y += $rowH New-Label 'Computer name' $colX $y | Out-Null $txtComputer = New-Field $fldX ($y + 26) $y += $rowH # --- right column: flavour + BitLocker ------------------------------------- $rX = 540; $rY = 180 $grpFlavour = New-Object Windows.Forms.GroupBox $grpFlavour.Text = 'Flavour' $grpFlavour.ForeColor = $colMuted $grpFlavour.Location = New-Object Drawing.Point($rX, $rY) $grpFlavour.Size = New-Object Drawing.Size(360, 220) $form.Controls.Add($grpFlavour) # 'essentials' is the always-on baseline role (no flavour manifest), not a selectable # flavour -- the four below are the real flavours; essentials apps ship with every one. $flavours = @('developer', 'journalist', 'daily-driver', 'privacy-max') $radioFlavours = @() $fy = 32 foreach ($f in $flavours) { $rb = New-Object Windows.Forms.RadioButton $rb.Text = $f; $rb.ForeColor = $colText; $rb.Font = $fontBase $rb.Location = New-Object Drawing.Point(20, $fy) $rb.Size = New-Object Drawing.Size(320, 28) $rb.Tag = $f if ($f -eq 'daily-driver') { $rb.Checked = $true } $grpFlavour.Controls.Add($rb) $radioFlavours += $rb $fy += 36 } $chkBitLocker = New-Object Windows.Forms.CheckBox $chkBitLocker.Text = 'Enable BitLocker (TPM + PIN)' $chkBitLocker.ForeColor = $colText; $chkBitLocker.Font = $fontBase $chkBitLocker.AutoSize = $true $chkBitLocker.Location = New-Object Drawing.Point($rX, ($rY + 240)) $form.Controls.Add($chkBitLocker) $lblPin = New-Label 'BitLocker PIN' $rX ($rY + 280) | Out-Null $lblPin = $form.Controls[$form.Controls.Count - 1] $txtPin = New-Field ($rX) ($rY + 306) 360 $true $lblPinC = New-Label 'Confirm PIN' $rX ($rY + 348) | Out-Null $lblPinC = $form.Controls[$form.Controls.Count - 1] $txtPinC = New-Field ($rX) ($rY + 374) 360 $true # PIN fields disabled until BitLocker is checked. $setPinEnabled = { $on = $chkBitLocker.Checked $txtPin.Enabled = $on; $txtPinC.Enabled = $on $fg = if ($on) { $colMuted } else { [Drawing.Color]::FromArgb(90, 90, 98) } $lblPin.ForeColor = $fg; $lblPinC.ForeColor = $fg } $chkBitLocker.Add_CheckedChanged($setPinEnabled) & $setPinEnabled # --- status line ----------------------------------------------------------- $lblStatus = New-Object Windows.Forms.Label $lblStatus.Text = '' $lblStatus.ForeColor = $colError; $lblStatus.Font = $fontBase $lblStatus.AutoSize = $false $lblStatus.Location = New-Object Drawing.Point(80, ($y + 8)) $lblStatus.Size = New-Object Drawing.Size(820, 28) $form.Controls.Add($lblStatus) # --- buttons --------------------------------------------------------------- function Style-Button($b, $primary) { $b.FlatStyle = 'Flat'; $b.Font = $fontBase $b.Size = New-Object Drawing.Size(150, 40) $b.FlatAppearance.BorderSize = 1 if ($primary) { $b.BackColor = $colAccent; $b.ForeColor = $colBg $b.FlatAppearance.BorderColor = $colAccent } else { $b.BackColor = $colPanel; $b.ForeColor = $colText $b.FlatAppearance.BorderColor = $colMuted } } $btnDefaults = New-Object Windows.Forms.Button $btnDefaults.Text = 'Use defaults' Style-Button $btnDefaults $false $btnDefaults.Location = New-Object Drawing.Point(80, ($y + 48)) $form.Controls.Add($btnDefaults) $btnCancel = New-Object Windows.Forms.Button $btnCancel.Text = 'Cancel' Style-Button $btnCancel $false $btnCancel.Location = New-Object Drawing.Point(580, ($y + 48)) $form.Controls.Add($btnCancel) $btnFinish = New-Object Windows.Forms.Button $btnFinish.Text = 'Finish' Style-Button $btnFinish $true $btnFinish.Location = New-Object Drawing.Point(750, ($y + 48)) $form.Controls.Add($btnFinish) # --- behaviour ------------------------------------------------------------- $btnDefaults.Add_Click({ $txtDisplay.Text = 'SilverMetal User' $txtUser.Text = 'silver' $txtComputer.Text = 'SILVER-PC' # Leave passwords blank on purpose: the user must set them. $txtPass.Text = ''; $txtPassC.Text = '' foreach ($rb in $radioFlavours) { $rb.Checked = ($rb.Tag -eq 'daily-driver') } $chkBitLocker.Checked = $false $txtPin.Text = ''; $txtPinC.Text = '' $lblStatus.ForeColor = $colMuted $lblStatus.Text = 'Defaults applied. Set a password to continue.' }) $btnCancel.Add_Click({ [Environment]::Exit(1) }) $form.Add_KeyDown({ if ($_.KeyCode -eq 'Escape') { [Environment]::Exit(1) } }) $btnFinish.Add_Click({ $lblStatus.ForeColor = $colError $rUser = Test-SmUsername $txtUser.Text if (-not $rUser.Ok) { $lblStatus.Text = $rUser.Message; return } $rPass = Test-SmPassword $txtPass.Text $txtPassC.Text if (-not $rPass.Ok) { $lblStatus.Text = $rPass.Message; return } $rComp = Test-SmComputerName $txtComputer.Text if (-not $rComp.Ok) { $lblStatus.Text = $rComp.Message; return } $blEnabled = $chkBitLocker.Checked $pin = '' if ($blEnabled) { $rPin = Test-SmPin $txtPin.Text $txtPinC.Text if (-not $rPin.Ok) { $lblStatus.Text = $rPin.Message; return } $pin = $txtPin.Text } $flavour = 'daily-driver' foreach ($rb in $radioFlavours) { if ($rb.Checked) { $flavour = [string]$rb.Tag } } $display = if ([string]::IsNullOrWhiteSpace($txtDisplay.Text)) { $txtUser.Text } else { $txtDisplay.Text } $lblStatus.ForeColor = $colAccent $lblStatus.Text = 'Generating answer file and starting Windows Setup...' $form.Refresh() $xml = New-SmAnswerFile -DisplayName $display -Username $txtUser.Text -Password $txtPass.Text ` -ComputerName $txtComputer.Text -Flavour $flavour ` -BitLockerEnable $blEnabled -BitLockerPin $pin Set-Content -Path 'X:\sm\unattend.generated.xml' -Value $xml -Encoding UTF8 try { [void][xml](Get-Content 'X:\sm\unattend.generated.xml' -Raw) } catch { [Environment]::Exit(1) } # bad XML -> fall back to default answer file $setup = if (Test-Path 'X:\sources\setup.exe') { 'X:\sources\setup.exe' } else { 'X:\setup.exe' } Start-Process -FilePath $setup -ArgumentList '/unattend:X:\sm\unattend.generated.xml' -Wait [Environment]::Exit(0) }) [void]$form.ShowDialog() # If the form closes without Finish/Cancel handling exiting, treat as cancel. [Environment]::Exit(1) } catch { # Any failure -> exit 1 so Start-Collector.cmd falls back to the default answer file. [Environment]::Exit(1) }